A modern web application built with a powerful tech stack combining server-side rendering and dynamic client interactions. This project leverages NestJS for the backend framework, Handlebars for templating, HTMX for seamless dynamic content updates, TailwindCSS with DaisyUI for styling, and Supabase for authentication and database management.
NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. It uses TypeScript by default and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).
How it's used in this project:
- Provides the core application structure with modules, controllers, and services
- Handles HTTP routing and request/response management
- Integrates with Express.js as the underlying HTTP server
- Manages dependency injection for clean, testable code
- Configuration in
src/main.tswhere the app is bootstrapped and view engine is set up
Handlebars is a minimal templating engine that provides the power necessary to let you build semantic templates effectively with no frustration.
How it's used in this project:
- Server-side rendering of HTML templates (
.hbsfiles) - Template files located in various view directories (e.g.,
src/auth/views/,src/dashboard/views/,src/main/views/) - Supports partials for reusable components (e.g.,
layout.hbs,sidebar-layout.hbs) - Dynamic content injection using Handlebars expressions
{{variable}} - Helper functions for conditional rendering and loops
- Configured in
src/main.tswithapp.setViewEngine('hbs')
Example template structure:
HTMX allows you to access modern browser features directly from HTML, rather than using JavaScript. It enables dynamic, interactive web applications with minimal JavaScript code.
How it's used in this project:
- Loaded via script tag in the main layout:
<script src='/htmx.js'></script> hx-boostattribute on the body tag enables progressive enhancement for navigation- Provides seamless page transitions without full page reloads
- Allows for partial HTML updates and dynamic content loading
- Enables SPA-like experiences while maintaining server-side rendering benefits
Key HTMX features utilized:
- hx-boost: Automatically converts links and forms to AJAX requests
- hx-get/hx-post: Make HTTP requests directly from HTML elements
- hx-target: Specify where response content should be inserted
- hx-swap: Control how content is swapped into the page
TailwindCSS is a utility-first CSS framework for rapidly building custom user interfaces.
How it's used in this project:
- Configured with custom build process:
npx @tailwindcss/cli -i ./tailwind/input.css -o ./public/output.css --watch - Provides utility classes for styling components
- Output CSS served from
/public/output.css - Enables responsive design with mobile-first approach
DaisyUI is a component library built on top of TailwindCSS, providing pre-designed UI components.
How it's used in this project:
- Installed as a dev dependency
- Provides styled components like buttons, cards, navbars, etc.
- Maintains Tailwind's utility-first approach while offering semantic component classes
Supabase is an open-source Firebase alternative providing authentication, database, and real-time subscriptions.
How it's used in this project:
- Client and SSR packages installed:
@supabase/supabase-jsand@supabase/ssr - Handles user authentication and session management
- Provides PostgreSQL database for data persistence
- Manages user credentials and secure authentication flows
- Node.js (v18 or higher recommended)
- npm or yarn package manager
- Supabase account (for authentication and database)
- Clone the repository:
git clone <repository-url>
cd Koshiki- Install dependencies:
npm install- Configure environment variables:
Create a
.envfile in the root directory with the following variables:
PORT=4000
SUPABASE_URL=your_supabase_url
SUPABASE_ANON_KEY=your_supabase_anon_key- Build TailwindCSS (run in a separate terminal):
npm run tailwindThis watches for changes and rebuilds your CSS automatically.
Start the NestJS application with hot-reload:
npm run start:devThe application will be available at http://localhost:4000 (or your configured PORT).
Note: Make sure to run npm run tailwind in a separate terminal to watch and compile CSS changes.
Build the application:
npm run buildStart the production server:
npm run start:prod# Development with auto-reload
npm run start:dev
# Development with debugging
npm run start:debug
# Build for production
npm run build
# Start production server
npm run start:prod
# Watch and compile TailwindCSS
npm run tailwind
# Format code with Prettier
npm run format
# Lint code
npm run lintThis project uses djlint for formatting .hbs (Handlebars) template files. djlint is a linter and formatter for HTML template languages.
Install djlint globally using pip:
pip install djlintOr using pipx (recommended):
pipx install djlintFormat a specific .hbs file:
djlint path/to/your/file.hbs --reformatFormat all .hbs files in a directory:
djlint src/ --reformat --extension=hbsCheck formatting without making changes:
djlint src/ --check --extension=hbsFormat all .hbs files in the project:
djlint . --reformat --extension=hbs--reformat: Format and modify files in place--check: Check files without modifying them (useful for CI/CD)--extension=hbs: Specify file extension to process--indent 2: Set indentation to 2 spaces (default is 4)
This application follows a modern web architecture pattern:
- NestJS Backend: Handles routing, business logic, and renders Handlebars templates
- Handlebars Templates: Generate HTML on the server with dynamic data
- HTMX: Enhances the frontend with dynamic interactions without writing JavaScript
- TailwindCSS + DaisyUI: Provides styling through utility classes and pre-built components
- Supabase: Manages authentication, database, and user sessions
User clicks a link with hx-boost
↓
HTMX intercepts the click and makes an AJAX request
↓
NestJS controller receives the request
↓
Controller fetches data from Supabase
↓
Controller renders Handlebars template with data
↓
HTMX receives the HTML response
↓
HTMX swaps the content into the page (no full reload)
This project uses a custom interceptor-based templating system rather than NestJS's built-in @Render() decorator. This provides greater flexibility for layout selection and dynamic rendering.
The core of the templating system is the HtmlInterceptor located in src/common/html.interceptor.ts. It intercepts controller responses and wraps content in the appropriate layout.
Key Components:
-
HtmlInterceptor (
src/common/html.interceptor.ts):- Processes responses from controllers decorated with
@InjectHtml() - Dynamically selects layouts based on device type or explicit selection
- Handles Handlebars compilation and partial registration
- Processes responses from controllers decorated with
-
Helper Functions (
src/common/helper.ts):koshiki(html, type, templateValues?): Prepares the response object for the interceptorgetLayoutType(userAgent): Determines layout based on device (mobile vs desktop)
-
Types (
src/common/types.ts):LayoutType:'sidebar' | 'card' | 'dock'- Available layout options
1. Request arrives at controller
↓
2. AuthInterceptor validates session (if not @Public)
↓
3. Controller handler executes
↓
4. Handler returns koshiki(template, layoutType, data)
↓
5. HtmlInterceptor processes response:
- Registers Handlebars helper with content data
- Selects layout based on layoutType
- Compiles and renders final HTML
↓
6. Complete HTML response sent to client
Important Interceptor Logic:
- The interceptor checks for
@InjectHtml()decorator metadata - It extracts
{ type, data }from the controller response datais a unique partial name registered bykoshiki()- The partial contains your compiled template with injected values
- Layout selection is dynamic: sidebar (desktop), dock (mobile), or card (centered)
- Create a controller with the interceptor decorators:
import { Controller, Get, Header, Headers } from '@nestjs/common';
import { InjectHtml } from '../common/inject-html.decorator';
import { koshiki, getLayoutType } from '../common/helper';
import { TemplateService } from '../template/template.service';
@Controller('example')
export class ExampleController {
constructor(private readonly template: TemplateService) {}
@Get()
@InjectHtml() // Enables the HtmlInterceptor
@Header('Content-Type', 'text/html')
getExample(@Headers() headers: Headers) {
// Determine layout based on device
const layout = getLayoutType(headers['user-agent']);
// Template data to inject
const data = {
message: 'Hello from NestJS!',
items: ['Item 1', 'Item 2', 'Item 3']
};
// Load your template string (from TemplateService or inline)
const templateHtml = `
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">{{message}}</h2>
<ul>
{{#each items}}
<li>{{this}}</li>
{{/each}}
</ul>
<button
hx-get="/api/data"
hx-target="#result"
class="btn btn-primary">
Load Data
</button>
<div id="result"></div>
</div>
</div>
`;
// koshiki compiles the template with data and prepares for interceptor
return koshiki(templateHtml, layout, data);
}
}- For static templates, add to TemplateService:
In src/template/template.service.ts:
@Injectable()
export class TemplateService implements OnModuleInit {
// ... existing templates
public examplePage: string;
onModuleInit() {
// ... existing template loading
this.examplePage = fs.readFileSync(
path.join(process.cwd(), 'src/example/views/example.hbs'),
'utf8',
);
}
}Then use in controller:
@Get()
@InjectHtml()
@Header('Content-Type', 'text/html')
getExample(@Headers() headers: Headers) {
const layout = getLayoutType(headers['user-agent']);
return koshiki(this.template.examplePage, layout, { message: 'Hello!' });
}sidebar: Desktop layout with navigation sidebar (used for main dashboard)card: Centered card layout (used for login/auth pages)dock: Mobile layout with bottom navigation dock- Automatic selection: Use
getLayoutType(userAgent)to automatically choose between sidebar and dock
- No
@Render()decorator - Uses@InjectHtml()instead - No direct template paths - Templates are loaded as strings via TemplateService
- Dynamic layouts - Layout selection happens at runtime based on device or logic
- Helper function pattern -
koshiki()prepares the response for the interceptor - Global interceptor - Registered once in
app.module.ts, works everywhere
Partials allow you to reuse template components:
<!-- Progressive enhancement with hx-boost -->
<body hx-boost>
<a href="/dashboard">Dashboard</a> <!-- Becomes AJAX request -->
</body>
<!-- Dynamic content loading -->
<button hx-get="/api/stats" hx-target="#stats">
Refresh Stats
</button>
<div id="stats"></div>
<!-- Form submission without page reload -->
<form hx-post="/api/submit" hx-target="#response">
<input type="text" name="data" />
<button type="submit">Submit</button>
</form>Koshiki/
├── src/
│ ├── auth/
│ │ └── views/ # Authentication templates
│ ├── dashboard/
│ │ └── views/ # Dashboard templates
│ ├── main/
│ │ └── views/ # Main layout templates
│ ├── app.module.ts # Root module
│ └── main.ts # Application entry point
├── public/ # Static assets (CSS, JS, images)
├── tailwind/
│ └── input.css # TailwindCSS source
├── views/ # Compiled view templates
└── package.json

