View Engines
KickJS supports server-side template rendering through the ViewAdapter. It works with any Express-compatible template engine, including EJS, Pug, Handlebars, and Nunjucks.
Why Use a View Engine?
While KickJS is primarily an API framework, some use cases benefit from server-rendered HTML:
- Admin dashboards that don't need a full SPA
- Email templates rendered before sending
- PDF generation from HTML templates
- Server-side rendered pages for SEO or lightweight UIs
- Error pages with styled HTML instead of raw JSON
The ViewAdapter registers a template engine with the underlying Express app and exposes ctx.render() in your controllers.
Installation
The ViewAdapter ships with @forinda/kickjs. Import it from the main barrel or the sub-path @forinda/kickjs/views. You only need to install the template engine of your choice:
pnpm add ejspnpm add pugpnpm add express-handlebarspnpm add nunjucksQuick Start (EJS)
1. Create a template
Create src/views/home.ejs:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1>Welcome, <%= name %>!</h1>
<p>Rendered by KickJS + EJS</p>
</body>
</html>2. Register the ViewAdapter
import ejs from 'ejs'
import { ViewAdapter } from '@forinda/kickjs/views'
import { bootstrap } from '@forinda/kickjs'
bootstrap({
modules,
adapters: [
ViewAdapter({
engine: ejs,
ext: 'ejs',
viewsDir: 'src/views',
}),
],
})3. Render from a controller
import { Controller, Get } from '@forinda/kickjs'
import type { RequestContext } from '@forinda/kickjs'
@Controller()
export class HomeController {
@Get('/')
index(ctx: RequestContext) {
ctx.render('home', { title: 'Home', name: 'World' })
}
}Visit http://localhost:3000/ and you will see the rendered HTML.
Pair with the Asset Manager
If you want type-safe references to individual templates (autocomplete + compile-time errors on typo'd template names), declare the views directory under assetMap in kick.config.ts and use assets.views.dashboard() instead of hand-rolling paths. See the Asset Manager guide. The two work together — ViewAdapter registers the engine + sets the views directory; the asset manager gives you typed references when your code resolves a specific template path.
ViewAdapter Options
| Option | Type | Default | Description |
|---|---|---|---|
engine | any | required | The template engine module or a custom render function |
ext | string | required | File extension for templates (e.g., 'ejs', 'pug', 'hbs') |
viewsDir | string | 'src/views' | Directory containing template files |
layout | string | undefined | Default layout template (engine-dependent) |
Engine Examples
EJS
import ejs from 'ejs'
import { ViewAdapter } from '@forinda/kickjs/views'
ViewAdapter({ engine: ejs, ext: 'ejs', viewsDir: 'src/views' })Template (src/views/dashboard.ejs):
<h1><%= title %></h1>
<ul>
<% for (const item of items) { %>
<li><%= item.name %> — <%= item.status %></li>
<% } %>
</ul>Pug
import pug from 'pug'
import { ViewAdapter } from '@forinda/kickjs/views'
ViewAdapter({ engine: pug, ext: 'pug', viewsDir: 'src/views' })Template (src/views/dashboard.pug):
h1= title
ul
each item in items
li #{item.name} — #{item.status}Handlebars
import { engine } from 'express-handlebars'
import { ViewAdapter } from '@forinda/kickjs/views'
ViewAdapter({ engine: engine(), ext: 'handlebars', viewsDir: 'src/views' })Template (src/views/dashboard.handlebars):
Nunjucks
import nunjucks from 'nunjucks'
import { ViewAdapter } from '@forinda/kickjs/views'
// Configure nunjucks with the views directory
nunjucks.configure('src/views', { autoescape: true })
ViewAdapter({ engine: nunjucks, ext: 'njk', viewsDir: 'src/views' })Template (src/views/dashboard.njk):
<h1>{{ title }}</h1>
<ul>
{% for item in items %}
<li>{{ item.name }} — {{ item.status }}</li>
{% endfor %}
</ul>Template File Structure
A typical views directory looks like this:
src/
views/
layout.ejs # Shared layout (header, footer, nav)
pages/
home.ejs
dashboard.ejs
settings.ejs
partials/
header.ejs
footer.ejs
sidebar.ejs
emails/
welcome.ejs
reset-password.ejsPassing Data to Templates
The second argument to ctx.render() is a plain object. All properties become available as local variables in the template:
@Get('/dashboard')
async dashboard(ctx: RequestContext) {
const user = await this.userService.findById(ctx.params.id)
const stats = await this.statsService.summary()
ctx.render('pages/dashboard', {
title: 'Dashboard',
user,
stats,
isAdmin: user.role === 'admin',
})
}In EJS, these are accessed directly:
<h1><%= title %></h1>
<p>Hello, <%= user.name %></p>
<% if (isAdmin) { %>
<a href="/admin">Admin Panel</a>
<% } %>Build-Time Folder Copying
Template files are not TypeScript, so they are not included in the Vite build output. Use the copyDirs option in kick.config.ts to copy your views directory into dist/ after each build:
// kick.config.ts
import { defineConfig } from '@forinda/kickjs-cli'
export default defineConfig({
copyDirs: [
'src/views', // copies to dist/src/views
{ src: 'src/views', dest: 'dist/views' }, // custom destination
'src/emails', // additional template dirs
],
})When you run kick build, the CLI copies these directories automatically after Vite finishes. See the CLI Commands page for the full kick.config.ts reference.
Related
- Adapters -- how the adapter lifecycle works
- Controllers & Routes -- controller decorators and
RequestContext - CLI Commands --
kick buildandcopyDirsconfiguration