English
Directory Structure
hazejs follows a ThinkPHP / Laravel-style layout: each kind of code has a home, and most of those homes are discovered automatically at build time. You rarely register anything by hand.
Backend-only (api template)
The backend source directory is the project root:
my-app/
├─ app/
│ ├─ channel/ Broadcast channel authorization (auto-discovered)
│ ├─ command/ Console commands (auto-discovered)
│ ├─ controller/ HTTP controllers
│ ├─ event/ Events (auto-discovered)
│ ├─ exception/
│ │ └─ Handler.ts Global exception handler (auto-discovered)
│ ├─ job/ Queue jobs (auto-discovered)
│ ├─ listener/ Event listeners (auto-discovered)
│ ├─ mail/ Mail definitions
│ ├─ message/ Microservice message handlers (auto-discovered; optional)
│ ├─ model/ ORM models
│ ├─ notification/ Notifications (auto-discovered)
│ ├─ policy/ Authorization policies (auto-discovered)
│ ├─ provider/ Service providers (auto-discovered)
│ ├─ resource/ API resources (response transformers)
│ ├─ response/
│ │ └─ Formatter.ts Response envelope (auto-discovered)
│ ├─ service/ Business services
│ ├─ validate/ Request validation schemas
│ └─ view/ TSX views (Welcome.tsx, Layout.tsx)
├─ config/ Configuration, one file per key (auto-discovered)
├─ database/
│ ├─ factories/ Model factories for tests and seeders (UserFactory.ts)
│ ├─ migrations/ SQL migrations generated by drizzle-kit
│ ├─ schema/ drizzle table definitions (users.ts, auth.ts)
│ └─ seeders/ Seed scripts for haze db:seed
├─ lang/
│ ├─ en/ Translations, one folder per locale (auto-discovered)
│ └─ zh-CN/
├─ modules/ Local modules, same layout as the app (auto-discovered; optional)
├─ route/ Route files (auto-discovered)
├─ schedule/ Scheduled tasks (auto-discovered)
├─ tests/ Vitest tests
├─ .env Environment variables (APP_KEY, DATABASE_URL, …)
├─ .env.test Overrides for tests
├─ haze.config.ts Toolchain config (srcDir, port, preset, …)
├─ package.json
├─ tsconfig.json
└─ vitest.config.tsFolders marked optional are not created by the template; add them when you need them. The same goes for app/middleware, app/interceptor and app/repository, described below.
Full-stack (vue / react templates)
In a full-stack project the backend moves into server/, and the root is a regular Vite project:
my-app/
├─ server/ Backend source directory (srcDir)
│ ├─ app/ Same folders as above, plus socket/
│ ├─ config/
│ ├─ database/
│ ├─ lang/
│ ├─ route/
│ └─ schedule/
├─ src/ Frontend (Vue or React)
│ ├─ api.ts fetch wrapper with the CSRF flow
│ ├─ socket.ts Socket.IO client
│ ├─ AiChat.vue Streaming AI chat (AiChat.tsx in React)
│ └─ main.ts / App.vue
├─ tests/ Backend tests
├─ index.html
├─ haze.config.ts srcDir: 'server'
├─ vite.config.ts Includes the haze() plugin
├─ tsconfig.json References tsconfig.app.json and tsconfig.server.json
└─ vitest.config.tsThe location is set by srcDir in haze.config.ts:
ts
// haze.config.ts
import { defineHazeConfig } from '@hazejs/vite'
export default defineHazeConfig({
srcDir: 'server',
})Everything described below as app/…, config/… and so on is relative to srcDir. A few things always stay at the project root: .env* files, tests/, stubs/, .haze/, dist/, and the default data/ and storage/ folders.
The ~ alias
~/ always points to srcDir, so import { User } from '~/app/model/User' works the same in both layouts. Configure it with the alias option of the haze() plugin.
What is auto-discovered
The Vite plugin builds a manifest of these files with import.meta.glob. Patterns with * scan only the first level of their directory — files in subdirectories are not loaded. The two ** patterns, app/command/** and lang/**, are recursive: the subdirectory becomes part of the command name or translation key.
Path (relative to srcDir) | Contents |
|---|---|
config/*.ts | Config files. The file name is the config key: config/app.ts → config('app.name') |
route/*.ts | Route files exporting defineRoutes(...) |
app/provider/*.ts | Service providers |
app/exception/Handler.ts | The global exception handler |
app/response/Formatter.ts | The response formatter |
app/interceptor/*.ts | Outbound interceptors, applied in file-name order |
app/policy/*.ts | Policies and abilities |
app/job/*.ts | Queue jobs. The file name is the job name |
app/command/**/*.ts | Console commands. The path is the command name (db/Cleanup.ts → db:cleanup); loaded only by haze and worker.mjs command |
app/event/*.ts | Events. The file name is the event name |
app/listener/*.ts | Listeners, run in file-name order |
schedule/*.ts | Scheduled tasks |
app/socket/*.ts | Socket.IO namespaces, read by @hazejs/socket.io |
app/notification/*.ts | Notifications. The file name is the notification type |
app/channel/*.ts | Broadcast channel authorization (defineChannels()) |
app/message/*.ts | Microservice message handlers, read by @hazejs/microservices |
app/view/**/*.tsx | TSX views, loaded on demand for ctx.view('Name') and haze down --render; the path is the view name (admin/Dashboard.tsx → admin/Dashboard) |
lang/**/*.{ts,json} | Translations: lang/<locale>/<file>.ts; the file path is the first part of the key (lang/en/admin/users.ts → admin.users.*) |
app/<name>/{route,config,provider,exception,response}/*.ts | Per-app modules for multiple apps |
modules/<name>/{module.ts,route/*,config/*,schedule/*,app/<kind>/*} | Local modules: the same kinds as the app (provider, interceptor, policy, job, event, listener, notification, channel, socket, message, and command/**) |
Adding or deleting one of these files in dev regenerates the manifest — no restart needed.
Directory reference
app/controller
Controllers receive the request, call services and return data. They are not discovered: routes reference them directly, as in r.get('/hello', [GreetingController, 'index']). Subdirectories are fine (app/controller/admin/UserController.ts). See Controllers.
app/service
Business logic that does not know about HTTP. Services are classes resolved by the container with inject(GreetingService). Bind them explicitly in a provider when you need a specific scope. See Service Container.
app/repository
Optional data-access layer, generated with haze make:repository. Not part of the templates.
app/model
ORM models built on the drizzle tables in database/schema, for example class User extends Model.define(users). See Models.
app/validate
Standard Schema validators (valibot by default; zod, arktype and others also work) used by .validate({ body, query, params }) and validate(). See Requests & Validation.
app/middleware
Class or function middleware. Middleware is not discovered: register an alias in config/middleware.ts and use it by name with .middleware('name'). See Middleware.
app/interceptor
Outbound interceptors receive the final Response and can change headers or replace it. Discovered, applied in file-name order.
app/policy
Authorization policies (definePolicy(Model, { … })) and model-free abilities (defineAbilities({ … })). Discovered and registered with the gate; ability names are included in the generated types. See Authorization.
app/job
Queue jobs defined with defineJob(). The file name becomes the job name. See Queues.
app/command
Console commands defined with defineCommand(), run with haze <name> or node dist/worker.mjs command <name>. Subdirectories are allowed and become part of the name. The templates ship Inspire.ts, run as haze inspire. See Console Commands.
app/event and app/listener
Events (defineEvent()) and listeners (defineListener(Event, handler)). Both are discovered; listeners can run synchronously or on the queue. See Events.
app/mail
Mail definitions (defineMail()), imported where you send them: Mail.to(user.email).send(WelcomeMail, payload). Not discovered. See Mail.
app/view
TSX views: components rendered to HTML on the server with ctx.view(<Home />). Import them where you render them, or render by name with ctx.view('admin/Dashboard', props). Layouts are components too. haze make:view Home creates app/view/Home.tsx.
app/notification
Notifications defined with defineNotification(), sent with notify(user, InvoicePaid(payload)) over mail, database, broadcast, Slack, webhooks and chat robots. They are discovered only to name them: the file name becomes the type stored in the database and pushed to the browser. See Notifications.
app/channel
Authorization for private and presence broadcast channels (defineChannels((c) => { c.private('orders.{id}', …) })). Every file is loaded; when at least one exists, the /broadcasting endpoints are enabled. See Broadcasting.
app/resource
API resources (defineResource((user, ctx) => ({ … }))) that decide which fields a model exposes: UserResource.make(user) for one item, UserResource.collection(await User.paginate(page, 20)) for lists and pages. Not discovered — import them in controllers. The templates ship UserResource.ts. See API Resources.
app/message
Microservice message handlers (defineMessageHandler('orders.find', handler)), registered by microservicesProvider() from @hazejs/microservices. Each file default-exports a handler or an array of handlers. They only receive messages while a listener runs (haze microservice:listen, or autoListen). See Microservices.
app/socket
Socket.IO namespaces (defineNamespace()), loaded by @hazejs/socket.io when its provider is registered. The file name is the namespace (chat.ts → /chat, index.ts → /). See Socket.IO.
app/provider
Service providers with register() (bind services) and boot() (startup logic). Every file is loaded — for example DatabaseProvider.ts (export default databaseProvider()) registers the database.
app/exception
Handler.ts extends ExceptionHandler to customize how errors are reported (report()) and rendered (render()). See Errors & Logging.
app/response
Formatter.ts extends ResponseFormatter to change the success and error envelope ({ code, message, data } by default). See Responses.
config/
One file per config key, usually with a defineXxxConfig() helper. See Configuration.
lang/
Translation files, one folder per locale: lang/en/messages.ts default-exports a nested object read with t('messages.welcome'). Subdirectories and JSON files work too. With config/i18n.ts present, validation errors and framework messages are translated as well. The templates ship en and zh-CN. See Localization.
route/
Centralized route definitions. Split them however you like (api.ts, ws.ts, …) — every file is loaded. See Routing.
database/
schema/— drizzle table definitions. drizzle-kit reads these files directly, so they should only import fromdrizzle-orm(do not use the~alias here).migrations/— SQL files andmeta/_journal.jsonwritten byhaze db:generate. Non-default connections usemigrations/<connection>/. Migrations are bundled into the production build.seeders/— files with a default-exported function, run in file-name order byhaze db:seed.factories/— model factories (defineFactory()), imported by tests and seeders; nothing loads them automatically.
See Database.
schedule/
Task schedules defined with defineSchedule((s) => { … }). See Task Scheduling.
modules/
Local modules: each modules/<name>/ folder uses the same layout as the app (route/, config/, app/…, schedule/, database/) plus an optional module.ts. They are enabled by default; config/modules.ts disables or configures them and enables module packages. Create one with haze make:module <name>.
tests/
Vitest tests using @hazejs/testing. Always at the project root, even in full-stack projects. See Testing.
Generated and runtime folders
| Path | Purpose | In git? |
|---|---|---|
.haze/ | Generated files: types for config keys, routes and abilities (types/haze.d.ts), translation keys (types/i18n.d.ts), the route list for the typed client (routes.ts); plus the last build record (build.json), local preview data and the REPL history | No |
dist/ | Build output (outDir in haze.config.ts) | No |
data/ | The template's SQLite database (DATABASE_URL=file:./data/app.db) | No |
storage/ | Local file storage disks (storage/app, storage/app/public), and the maintenance state file storage/framework/down | No |
stubs/ | Your customized generator templates, created by haze stub:publish | Yes |
Include .haze/types/**/*.d.ts in your tsconfig.json (the templates already do) so typed config keys, route names, abilities and translation keys are picked up by the editor. haze dev regenerates these files as you edit; run haze typegen in CI.