English
Getting Started
This guide creates a new hazejs project, runs it, and walks through adding your first route and controller.
Versioning
All @hazejs/* packages and create-hazejs share one version number and follow semantic versioning. Upgrade them together, and check the changelog for what changed.
Prerequisites
- Node.js 22.12 or later (required by Vite 8).
- pnpm is recommended. npm and yarn work too.
Create a project
sh
pnpm create hazejs my-appsh
npm create hazejs@latest my-appsh
yarn create hazejs my-appWithout a -t flag you are asked for a template interactively. Pass it explicitly to skip the prompt:
sh
pnpm create hazejs my-app -t api # backend-only API
pnpm create hazejs my-app -t vue # Vue 3 full-stack
pnpm create hazejs my-app -t react # React 19 full-stackTIP
With npm, separate the flags from the package name: npm create hazejs@latest my-app -- -t vue.
| Template | What you get |
|---|---|
api | A backend-only service. Code lives at the project root (app/, config/, route/). |
vue | Vue 3 + hazejs in one repository, one port and one build. The backend lives in server/. |
react | The same full-stack setup with React 19. |
module | A publishable module package (routes, middleware, config and migrations) that other projects enable in config/modules.ts. |
In non-interactive environments (CI, no TTY) the api template is used by default. Pass --force to scaffold into a non-empty directory (its contents are removed first), and --auto-import to turn on auto imports so controllers, services and framework APIs need no import statements.
The scaffolder:
- copies the template and sets the
nameinpackage.jsonfrom the directory name; - writes a fresh, random
APP_KEYinto.env— it signs cookies, encrypts sessions and is the default JWT secret; - adds a
pnpm-workspace.yamlthat declares how dependency build scripts are handled (pnpm 11 requires it), unless the project is created inside an existing pnpm workspace.
It does not install dependencies. Do that next:
sh
cd my-app
pnpm installWhat's inside
The api template is a complete, working example — not an empty shell. It includes:
GET /api/helloandPOST /api/hellowith request validation;- a
usersresource backed by a SQLite database and aUsermodel, shaped by an API resource (app/resource/UserResource.ts) that shows the email only to the user and admins; - JWT registration, login and
/api/auth/me, with a policy that lets users edit only their own profile; - account endpoints under
/api/auth: personal access tokens (/tokens), email verification, password reset, and GitHub login that turns on onceGITHUB_CLIENT_ID/GITHUB_CLIENT_SECRETare set in.env(config/oauth.ts,@hazejs/oauth); - a streaming AI chat endpoint,
POST /api/chat(config/ai.ts); - an event, listeners, a queued job, a mail and a scheduled task;
- a notification (
WelcomeNotification, sent by mail and broadcast) and a private broadcast channel for each user (app/channel/user.ts); - translations in
lang/enandlang/zh-CNwithconfig/i18n.ts, so the welcome mail, validation errors and framework messages follow the request's language (Localization); - an example console command,
haze inspire(app/command/Inspire.ts); - a WebSocket route at
/api/ws; - tests for all of the above.
The full-stack templates add a Vue or React frontend in src/ (including an AI chat component) and a Socket.IO namespace. See Directory Structure for a tour of every folder.
Run it
The template uses SQLite (data/app.db). Create the tables once before the first run, then start the dev server:
sh
pnpm db:migrate
pnpm devThe api template listens on http://localhost:3000:
sh
curl localhost:3000/api/hello
# {"code":0,"message":"ok","data":{"app":"my-app","message":"Hello, hazejs!"}}Full-stack projects run through Vite instead, on Vite's port (http://localhost:5173 by default) — the page and /api/** share it. See Full-stack with Vite.
A few pages come with no setup. In development, /__haze opens the dev dashboard (requests, queries, jobs, mail, logs) and /docs shows the OpenAPI docs generated from your routes; /health answers health checks in every mode. In full-stack projects the Vite dev server only passes /api/** and /__haze to the backend, so move the OpenAPI paths (path and docsPath in config/openapi.ts) and the health path (config/health.ts) under /api if you need them there.
Edit any controller, service, route or config file and the change is live on the next request. The process does not restart; the terminal prints a reloaded line instead.
Your first route and controller
Generate a service and a controller:
sh
npx haze make:service Post # app/service/PostService.ts
npx haze make:controller Post # app/controller/PostController.tsThe service holds business logic and knows nothing about HTTP:
ts
// app/service/PostService.ts
export class PostService {
private posts = [
{ id: 1, title: 'Hello hazejs' },
{ id: 2, title: 'Second post' },
]
all() {
return this.posts
}
find(id: number) {
return this.posts.find(post => post.id === id)
}
}The controller receives the request, calls the service through inject(), and returns plain data:
ts
// app/controller/PostController.ts
import type { HttpContext } from '@hazejs/core'
import { inject, NotFoundException } from '@hazejs/core'
import { PostService } from '~/app/service/PostService'
export class PostController {
private posts = inject(PostService)
index() {
return this.posts.all()
}
show(ctx: HttpContext) {
const post = this.posts.find(Number(ctx.param('id')))
if (!post)
throw new NotFoundException(`Post ${ctx.param('id')} not found`)
return post
}
}~ is an alias for the backend source directory, so imports stay the same wherever a file lives. Routes are not discovered from controllers — register them in route/api.ts, inside the existing /api group:
ts
// route/api.ts
import { PostController } from '~/app/controller/PostController'
export default defineRoutes((r) => {
r.group({ prefix: '/api', as: 'api.' }, (r) => {
// ...existing routes
r.get('/posts', [PostController, 'index']).name('posts.index')
r.get('/posts/:id', [PostController, 'show']).name('posts.show')
})
})Save, and try it:
sh
curl localhost:3000/api/posts/1
# {"code":0,"message":"ok","data":{"id":1,"title":"Hello hazejs"}}
curl localhost:3000/api/posts/9
# 404, rendered by the exception handler in the same envelopeRun pnpm routes to see the new entries in the route table. Because the routes are named, router.url('api.posts.show', { id: 1 }) is type-checked once types are generated (the dev server does this automatically).
Add a test
sh
npx haze make:test post # tests/post.test.tsts
// tests/post.test.ts
import { useTestApp } from '@hazejs/testing'
import { describe, expect, it } from 'vitest'
const t = useTestApp()
describe('posts', () => {
it('shows a post', async () => {
const res = await t.get('/api/posts/1')
expect(res.status).toBe(200)
expect(res.body.data).toEqual({ id: 1, title: 'Hello hazejs' })
})
})sh
pnpm testTests boot the real application in-process — no port is opened. See Testing.
Package scripts
Every template ships with these scripts:
| Script | Command | Purpose |
|---|---|---|
dev | haze dev | Dev server with in-place hot reload |
build | haze build | Production build (dist/) |
start | haze start | Run the production build |
preview | haze preview | Run the build locally with its deployment preset |
queue | haze queue:work | Consume queued jobs |
schedule | haze schedule:work | Run scheduled tasks every minute |
test | haze test | Run Vitest |
routes | haze route:list | Print the route table |
typecheck | tsc --noEmit | Type check (vue-tsc --build / tsc --build in full-stack templates) |
typegen | haze typegen | Generate types for config keys, routes, abilities and translation keys, plus the route list for the typed client |
db:generate | haze db:generate | Generate a migration from schema changes |
db:migrate | haze db:migrate | Run pending migrations |
db:seed | haze db:seed | Run seeders |
Extra flags are passed through, for example pnpm db:generate --name create_posts. The CLI Reference documents every command.
AI coding agents
hazejs ships Agent Skills that teach Claude Code, Cursor, Codex and other skill-aware coding agents the framework's conventions and APIs. Install them into your project with the skills CLI:
sh
npx skills add penna-org/hazeThe command lists the skills and asks which ones and which agents to install for:
| Skill | Use it for |
|---|---|
hazejs | Building apps: routing, validation, database, auth, queues, realtime, AI, testing, deployment and the rest of the framework |
hazejs-migration | Porting a Fastify, Express, Koa, Hono or NestJS backend to hazejs while keeping its API contract |
hazejs-contributing | Working on the framework itself (the penna-org/haze repository) |
Pick skills and agents up front with --skill hazejs --agent claude-code, add -g to install for your user instead of the project, and run npx skills update after upgrading hazejs. The DevTools MCP endpoint lets the agent inspect the running app as well.
Next steps
- Directory Structure — what each folder is for
- Configuration —
config/*.ts,.envandhaze.config.ts - Routing and Controllers — the HTTP layer in depth
- Database — schemas, migrations and models
- API Resources and OpenAPI — shape responses, document the API and call it from a typed client
- Authentication, API Tokens, Two-Factor & Passkeys and OAuth Social Login — the account features the template starts with
- Notifications and Broadcasting — reach users by mail, chat apps and realtime channels
- Localization — translations and per-request language
- Console Commands — your own
hazecommands and the REPL - Observability — health checks, OpenTelemetry and the dev dashboard
- Testing — the test client and fakes
- Deployment — build for Node, Docker, serverless or the edge