Skip to content

MVC ​

Beyond creating REST APIs, HonestJS supports building traditional Model-View-Controller (MVC) applications with server-side rendered views, powered by Hono's JSX engine and the JsxRenderer middleware.

Core Concepts ​

The MVC support in HonestJS is built around a few specialized decorators and concepts that extend the core framework:

  • Views: These are special controllers designed for rendering UI. They are decorated with @View() instead of @Controller().
  • Page Decorator: A custom HTTP method decorator, @Page(), is used within Views to signify a method that renders a page. It's essentially a specialized @Get() decorator.
  • JsxRenderer Middleware: The JsxRendererMiddleware provides JSX rendering capabilities with automatic layout wrapping.
  • Layout Component: The Layout component provides comprehensive HTML document structure with SEO optimization and modern web standards support.
  • JSX and Components: You can use JSX (.tsx) to define your components and layouts, which are then rendered to HTML.

Layout Component ​

The Layout component is a powerful server-side rendering utility that provides a comprehensive HTML document structure with SEO optimization, flexible configuration, and modern web standards support.

Overview ​

The Layout component is designed for building full-stack applications with HonestJS, providing a clean way to generate complete HTML documents with proper meta tags, scripts, stylesheets, and content. It works seamlessly with the JsxRenderer middleware to provide automatic layout wrapping.

Basic Usage ​

typescript
import { Layout } from 'honestjs'

const html = Layout({
	title: 'My Application',
	description: 'A modern web application built with HonestJS',
	children: '<h1>Hello World</h1>'
})

Configuration Options ​

The Layout component accepts a comprehensive configuration object:

typescript
interface SiteData {
	title: string // Required: Page title
	description?: string // Page description
	image?: string // Open Graph and Twitter image URL
	url?: string // Canonical URL
	locale?: string // Page locale (defaults to 'en_US')
	type?: string // Open Graph type (defaults to 'website')
	siteName?: string // Site name for Open Graph
	customMeta?: MetaTag[] // Array of custom meta tags
	scripts?: (string | ScriptOptions)[] // Array of script URLs or objects
	stylesheets?: string[] // Array of stylesheet URLs
	favicon?: string // Favicon URL
	twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'
	csp?: string // Content Security Policy
	htmlAttributes?: HtmlAttributes // Custom HTML attributes
	headAttributes?: HtmlAttributes // Custom head attributes
	bodyAttributes?: HtmlAttributes // Custom body attributes
}

SEO Optimization ​

The Layout component automatically generates comprehensive SEO meta tags:

Basic SEO ​

typescript
const html = Layout({
	title: 'Product Page',
	description: 'Amazing product with great features',
	url: 'https://example.com/product',
	siteName: 'My Store'
})

Open Graph Tags ​

typescript
const html = Layout({
	title: 'Product Page',
	description: 'Amazing product with great features',
	image: 'https://example.com/product.jpg',
	url: 'https://example.com/product',
	type: 'product',
	siteName: 'My Store'
})

Twitter Cards ​

typescript
const html = Layout({
	title: 'Product Page',
	description: 'Amazing product with great features',
	image: 'https://example.com/product.jpg',
	twitterCard: 'summary_large_image'
})

Custom Meta Tags ​

typescript
const html = Layout({
	title: 'Product Page',
	customMeta: [
		{ property: 'og:price:amount', content: '29.99' },
		{ property: 'og:price:currency', content: 'USD' },
		{ name: 'keywords', content: 'product, amazing, features' },
		{ name: 'author', content: 'John Doe' }
	]
})

Script and Stylesheet Management ​

Basic Scripts and Stylesheets ​

typescript
const html = Layout({
	title: 'My App',
	scripts: ['/app.js', '/analytics.js'],
	stylesheets: ['/styles.css', '/components.css']
})

Advanced Script Configuration ​

typescript
const html = Layout({
	title: 'My App',
	scripts: [
		'/app.js',
		{ src: '/analytics.js', async: true },
		{ src: '/critical.js', defer: true },
		{ src: '/lazy.js', async: true, defer: true }
	],
	stylesheets: ['/styles.css', '/print.css']
})

Content Security Policy ​

typescript
const html = Layout({
	title: 'Secure App',
	csp: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
})

Custom Attributes ​

typescript
const html = Layout({
	title: 'My App',
	htmlAttributes: {
		lang: 'en',
		'data-theme': 'dark'
	},
	headAttributes: {
		'data-head': 'true'
	},
	bodyAttributes: {
		class: 'app-body',
		'data-page': 'home'
	}
})

Complete Layout Example ​

Here's a comprehensive example showing all features:

typescript
import { Layout } from 'honestjs'

const html = Layout({
	title: 'HonestJS - Modern Web Framework',
	description: 'A lightweight, fast web framework built on Hono with TypeScript support',
	image: 'https://honestjs.dev/og-image.png',
	url: 'https://honestjs.dev',
	locale: 'en_US',
	type: 'website',
	siteName: 'HonestJS',
	favicon: '/favicon.ico',
	twitterCard: 'summary_large_image',
	csp: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;",
	scripts: ['/app.js', { src: '/analytics.js', async: true }, { src: '/critical.js', defer: true }],
	stylesheets: ['/styles.css', '/components.css'],
	customMeta: [
		{
			name: 'keywords',
			content: 'web framework, typescript, hono, decorators'
		},
		{ name: 'author', content: 'HonestJS Team' },
		{ property: 'og:site_name', content: 'HonestJS' }
	],
	htmlAttributes: {
		lang: 'en',
		'data-framework': 'honestjs'
	},
	headAttributes: {
		'data-head': 'true'
	},
	bodyAttributes: {
		class: 'app-body',
		'data-page': 'home'
	},
	children: `
		<header>
			<h1>Welcome to HonestJS</h1>
		</header>
		<main>
			<p>A modern web framework for TypeScript and JavaScript</p>
		</main>
		<footer>
			<p>&copy; 2024 HonestJS</p>
		</footer>
	`
})

Integration with Controllers and JsxRenderer ​

You can use the Layout component in your controllers with the JsxRenderer middleware:

typescript
import { Controller, Get, Ctx } from 'honestjs'
import type { Context } from 'hono'

@Controller('pages')
export class PagesController {
	@Get('home')
	home(@Ctx() ctx: Context) {
		return ctx.render(
			<div>
				<h1>Welcome to My App</h1>
				<p>Built with HonestJS</p>
			</div>,
			{
				title: 'Home - My App',
				description: 'Welcome to our application',
			}
		)
	}

	@Get('about')
	about(@Ctx() ctx: Context) {
		return ctx.render(
			<div>
				<h1>About Us</h1>
				<p>We are a modern web development company.</p>
			</div>,
			{
				title: 'About - My App',
				description: 'Learn more about our company',
			}
		)
	}
}

Dynamic Content ​

You can generate dynamic content based on data:

typescript
@Controller('products')
export class ProductsController {
	@Get(':id')
	async product(@Ctx() ctx: Context, @Param('id') id: string) {
		const product = await this.productService.findById(id)

		return ctx.render(
			<div>
				<h1>{product.name}</h1>
				<p>{product.description}</p>
				<p>${product.price}</p>
			</div>,
			{
				title: `${product.name} - My Store`,
				description: product.description,
				image: product.image,
				url: `https://mystore.com/products/${id}`,
				type: 'product',
				customMeta: [
					{ property: 'og:price:amount', content: product.price.toString() },
					{ property: 'og:price:currency', content: 'USD' },
				],
			}
		)
	}
}

Layout Best Practices ​

1. Always Provide a Title ​

The title is required and crucial for SEO:

typescript
// ✅ Good
ctx.render(<div>Content</div>, {
	title: 'Page Title',
})

// ❌ Avoid
ctx.render(<div>Content</div>)

2. Use Descriptive Descriptions ​

Provide meaningful descriptions for better SEO:

typescript
ctx.render(<div>Content</div>, {
	title: 'Product Page',
	description: 'High-quality product with amazing features and competitive pricing',
})

3. Include Open Graph Images ​

Add images for better social media sharing:

typescript
ctx.render(<div>Content</div>, {
	title: 'Product Page',
	description: 'Amazing product',
	image: 'https://example.com/product.jpg',
})

4. Optimize Script Loading ​

Use appropriate loading strategies for scripts:

typescript
ctx.render(<div>Content</div>, {
	title: 'My App',
	scripts: [
		{ src: '/critical.js', defer: true }, // Load early but don't block
		{ src: '/analytics.js', async: true }, // Load in parallel
		{ src: '/lazy.js', defer: true }, // Load after page
	],
})

5. Set Proper Viewport ​

The Layout component automatically includes the viewport meta tag for responsive design:

typescript
// Automatically included:
// <meta name="viewport" content="width=device-width, initial-scale=1.0" />

6. Use Content Security Policy ​

Implement CSP for better security:

typescript
ctx.render(<div>Content</div>, {
	title: 'Secure App',
	csp: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';",
})

Performance Considerations ​

1. Minimize Scripts ​

Only include necessary scripts:

typescript
// ✅ Good - Only essential scripts
ctx.render(<div>Content</div>, {
	scripts: ['/app.js', '/analytics.js'],
})

// ❌ Avoid - Too many scripts
ctx.render(<div>Content</div>, {
	scripts: ['/app.js', '/lib1.js', '/lib2.js', '/lib3.js', '/lib4.js', '/lib5.js'],
})

2. Use Async/Defer Appropriately ​

Choose the right loading strategy:

typescript
ctx.render(<div>Content</div>, {
	scripts: [
		{ src: '/critical.js', defer: true }, // Critical functionality
		{ src: '/analytics.js', async: true }, // Non-critical tracking
		{ src: '/lazy.js', defer: true }, // Lazy-loaded features
	],
})

3. Optimize Images ​

Use optimized images for Open Graph:

typescript
ctx.render(<div>Content</div>, {
	image: 'https://example.com/optimized-image-1200x630.jpg', // Optimal size for social sharing
})

MVC Decorators ​

HonestJS provides several decorators specifically for MVC applications:

@View(route?, options?) ​

An alias for @Controller with MVC naming conventions. Views are typically configured to ignore global prefixes and versioning, making them suitable for top-level page routes.

typescript
import { View } from 'honestjs'

@View('pages')
class PagesController {
	// This controller handles page rendering
}

@Page(path?, options?) ​

An alias for @Get with MVC naming conventions. Used to clearly indicate that a method renders a view.

typescript
import { View, Page } from 'honestjs'

@View('pages')
class PagesController {
	@Page('home')
	home() {
		// Renders the home page
	}
}

@MvcModule(options) ​

An enhanced module decorator with view support. It automatically includes views in the controllers array.

typescript
import { MvcModule } from 'honestjs'

@MvcModule({
	views: [PagesController],
	controllers: [ApiController],
	services: [DataService]
})
class AppModule {}

Setting up an MVC Application ​

1. Project Configuration ​

Your tsconfig.json needs to be configured to support JSX:

json
{
	"compilerOptions": {
		"jsx": "react-jsx",
		"jsxImportSource": "hono/jsx"
	}
}

2. Creating Custom Layouts with JSX ​

Create custom layouts using JSX components for better type safety and maintainability:

tsx
import { Layout, type SiteData } from 'honestjs'
import type { PropsWithChildren } from 'hono/jsx'
import { Footer } from '../components/Footer'
import { Header } from '../components/Header'

export const MainLayout = ({ children, stylesheets, scripts, ...props }: PropsWithChildren<SiteData>) => {
	const globalStylesheets: string[] = ['/static/css/main.css']
	const globalScripts: string[] = ['/static/js/main.js']

	return (
		<Layout
			{...props}
			stylesheets={[...globalStylesheets, ...(stylesheets || [])]}
			scripts={[...globalScripts, ...(scripts || [])]}
		>
			<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
				<Header />
				<main style={{ flex: 1, padding: '2rem 0' }}>{children}</main>
				<Footer />
			</div>
		</Layout>
	)
}

3. Setting up JSX Rendering ​

Configure the JsxRenderer middleware in your application:

typescript
import 'reflect-metadata'
import { Application } from 'honestjs'
import { JsxRendererMiddleware } from '@honestjs/middleware'
import AppModule from './app.module'
import { MainLayout } from './layouts/MainLayout'

declare module 'hono' {
	interface ContextRenderer {
		(content: string | Promise<string>, props: SiteData): Response
	}
}

const { hono } = await Application.create(AppModule, {
	hono: { strict: true },
	routing: { prefix: 'api', version: 1 },
	components: {
		middleware: [new JsxRendererMiddleware(MainLayout)]
	}
})

export default hono

4. Using Custom Layouts in Views ​

You can use your custom layouts in views by rendering JSX components. The Layout component (documented above) provides the foundation for creating consistent HTML structure across your application:

tsx
import { Ctx, Page, View } from 'honestjs'
import type { Context } from 'hono'
import { UserList } from './components/UserList'
import UsersService from './users.service'

@View('/users')
class UsersView {
	stylesheets: string[] = ['/static/css/views/users.css']
	scripts: string[] = ['/static/js/views/users.js']

	constructor(private readonly usersService: UsersService) {}

	@Page()
	async index(@Ctx() ctx: Context) {
		const users = await this.usersService.findAll()

		return ctx.render(<UserList users={users} />, {
			title: 'Users',
			description: 'List of users',
			stylesheets: this.stylesheets,
			scripts: this.scripts
		})
	}
}

5. Creating Reusable Components ​

You can create reusable components using JSX with proper TypeScript types:

tsx
import { memo } from 'hono/jsx'

export const Header = memo(() => {
	return (
		<header>
			<h1>Honest.js MVC</h1>
		</header>
	)
})
tsx
export const Footer = memo(() => {
	return (
		<footer>
			<p>© {new Date().getFullYear()} Company. All rights reserved.</p>
		</footer>
	)
})
tsx
import type { FC } from 'hono/jsx'
import type { User } from '../models/user.model'

interface UserListProps {
	users: User[]
}

export const UserList: FC<UserListProps> = (props: UserListProps) => {
	return (
		<div>
			<h2>All Users</h2>
			{props.users.length === 0 ? (
				<div>
					<h3>No users yet</h3>
					<p>Get started by adding your first user</p>
				</div>
			) : (
				props.users.map((user) => (
					<div key={user.id}>
						<h3>{user.name}</h3>
						{user.email && <p>{user.email}</p>}
						{user.role && <span>{user.role}</span>}
					</div>
				))
			)}
		</div>
	)
}

Module Configuration ​

MVC Module Setup ​

typescript
import { MvcModule } from 'honestjs'
import UsersController from './users.controller'
import UsersService from './users.service'
import UsersView from './users.view'

@MvcModule({
	views: [UsersView],
	controllers: [UsersController],
	services: [UsersService]
})
class UsersModule {}

export default UsersModule

App Module Configuration ​

typescript
import { Module } from 'honestjs'
import UsersModule from './modules/users/users.module'

@Module({
	imports: [UsersModule]
})
class AppModule {}

export default AppModule

Combining API and Views ​

You can have both API controllers and view controllers in the same application:

typescript
// API Controller
@Controller('users')
class UsersController {
	constructor(private readonly usersService: UsersService) {}

	@Get()
	async getUsers(): Promise<User[]> {
		return await this.usersService.findAll()
	}

	@Post()
	async createUser(@Body() body: CreateUserDto): Promise<User> {
		return await this.usersService.create(body)
	}
}
typescript
// View Controller
@View('/users')
class UsersView {
	stylesheets: string[] = ['/static/css/views/users.css']
	scripts: string[] = ['/static/js/views/users.js']

	constructor(private readonly usersService: UsersService) {}

	@Page()
	async index(@Ctx() ctx: Context) {
		const users = await this.usersService.findAll()

		return ctx.render(<UserList users={users} />, {
			title: 'Users',
			description: 'List of users',
			stylesheets: this.stylesheets,
			scripts: this.scripts,
		})
	}
}

The `@View` decorator is just a shortcut for `@Controller` without prefix and versioning. Make sure to add

versioning or prefix to the API controllers to avoid conflicts.

Service Layer ​

The service layer handles business logic and data operations:

typescript
import { Service } from 'honestjs'
import { CreateUserDto } from './dtos/create-user.dto'
import { User } from './models/user.model'

@Service()
class UsersService {
	private users: User[] = [
		{ id: 1, name: 'John', email: '[email protected]', role: 'admin' },
		{ id: 2, name: 'Jane', email: '[email protected]', role: 'admin' }
	]

	async create(user: CreateUserDto): Promise<User> {
		const id = this.users.length + 1
		this.users.push({
			id,
			name: user.name,
			email: user.email,
			role: 'user'
		})
		return this.users[id - 1]
	}

	async findAll(): Promise<User[]> {
		return this.users
	}

	async findById(id: number): Promise<User | null> {
		return this.users.find((user) => user.id === id) || null
	}
}

export default UsersService

Best Practices ​

1. Separate API and View Controllers ​

Keep API controllers and view controllers separate for better organization:

typescript
// API for data
@Controller('users', { prefix: 'api', version: 1 })
class UsersApiController {
	@Get()
	async getUsers() {
		return await this.usersService.findAll()
	}
}
typescript
// Views for UI
@View('users')
class UsersView {
	@Page()
	async list() {
		// Render the users page
	}
}

2. Use Custom Layouts for All Pages ​

Always use custom layouts for consistent HTML structure. The Layout component provides comprehensive HTML document structure with SEO optimization:

tsx
@Page('home')
async home(@Ctx() ctx: Context) {
	return ctx.render(
		<div>
			<h1>Welcome</h1>
		</div>,
		{
			title: 'Home',
			description: 'Welcome to our app'
		}
	)
}

3. Leverage SEO Features ​

Take advantage of the Layout component's comprehensive SEO features including Open Graph tags, Twitter Cards, and custom meta tags:

tsx
return ctx.render(<UserList users={users} />, {
	title: 'Page Title',
	description: 'Page description',
	image: 'https://example.com/image.jpg',
	url: 'https://example.com/page',
	type: 'website'
})

4. Use JSX Components for Reusability ​

Create reusable JSX components for common UI elements:

tsx
import { memo } from 'hono/jsx'

export const Header = memo(() => (
	<header>
		<nav>
			<a href="/">Home</a>
			<a href="/about">About</a>
			<a href="/contact">Contact</a>
		</nav>
	</header>
))
tsx
export const Footer = memo(() => (
	<footer>
		<p>&copy; {new Date().getFullYear()} My App</p>
	</footer>
))

5. Handle Dynamic Data with JSX ​

Use services to fetch data for your views with JSX components:

tsx
import type { FC } from 'hono/jsx'
import type { User } from '../models/user.model'

interface DashboardProps {
	users: User[]
	stats: { totalUsers: number; activeUsers: number }
}

export const Dashboard: FC<DashboardProps> = ({ users, stats }) => {
	return (
		<div>
			<h1>Dashboard</h1>
			<div>
				<p>Total Users: {stats.totalUsers}</p>
				<p>Active Users: {stats.activeUsers}</p>
			</div>
			<div>
				<h2>Recent Users</h2>
				{users.map((user) => (
					<p key={user.id}>{user.name}</p>
				))}
			</div>
		</div>
	)
}
tsx
@View('/dashboard')
class DashboardView {
	constructor(
		private readonly userService: UserService,
		private readonly statsService: StatsService
	) {}

	@Page()
	async dashboard(@Ctx() ctx: Context) {
		const [users, stats] = await Promise.all([
			this.userService.getRecentUsers(),
			this.statsService.getDashboardStats()
		])

		return ctx.render(<Dashboard users={users} stats={stats} />, {
			title: 'Dashboard'
		})
	}
}

With these MVC features, you can build powerful full-stack applications that combine the robust backend features of HonestJS with flexible server-side rendering capabilities using JSX and the JsxRenderer middleware.

Released under the MIT License.