Directory Structure
A freshly scaffolded Zerotal app organizes code into predictable directories, most of which are auto-discovered and wired by convention so you rarely touch the bootstrap files.
# project root
my-app/
├── app/
│ ├── controllers/
│ ├── events/ ← event classes (auto-discovered)
│ ├── exceptions/
│ │ └── Handler.ts
│ ├── jobs/
│ ├── listeners/ ← event listeners (auto-discovered)
│ ├── middleware/
│ ├── models/ ← ORM models (auto-discovered)
│ ├── observers/ ← model observers (auto-discovered)
│ ├── policies/ ← authorization policies (auto-discovered)
│ ├── providers/
│ │ └── AppServiceProvider.ts
│ ├── flow/ ← Flow Pages (server-driven UI)
│ │ └── pages/
│ └── requests/ ← FormRequest validation classes
├── bootstrap/
│ ├── app.ts ← Application bootstrap
│ └── providers.ts ← Provider registry
├── config/
│ ├── app.ts
│ ├── database.ts
│ ├── mail.ts
│ └── ...
├── database/
│ ├── migrations/
│ ├── seeders/
│ └── factories/
├── docs/ ← Optional: serve with Router.markdown()
├── public/ ← Static assets
│ └── uploads/
├── resources/
│ └── views/ ← JSX / TSX view components
├── routes/
│ └── index.ts ← Explicit route definitions
├── tests/
│ ├── Feature/
│ └── Unit/
├── .env
├── .env.example
├── bun.lockb
├── package.json
└── zt.ts ← CLI entry point (don't edit)
Key directories
app/
Application code lives here. Zerotal uses no magic autoloading — you import what you need explicitly, so names and locations are entirely up to you.
app/controllers/ — HTTP controller classes. Each method maps to a route
action. Name them freely; the framework just needs the class and method name at
Router.get('/path', MyController, 'method').
app/exceptions/Handler.ts — your custom exception handler. Extends
ExceptionHandler from zerotal. Registered in bootstrap/app.ts via
app.withExceptionHandler(Handler).
app/jobs/ — background job classes for @zerotal/queue.
app/middleware/ — custom middleware classes implementing Pipe<HttpContext>.
app/models/ — ORM model classes extending Model from @zerotal/orm.
Auto-discovered at boot; the table name is derived by convention, so @table is optional.
app/providers/ — service providers for registering custom bindings and
booting application services.
app/flow/pages/ — Flow Component classes. When using file-based routing these
are scanned automatically and registered as reactive WebSocket routes.
app/requests/ — FormRequest validation classes. Group by domain:
app/requests/posts/StorePostRequest.ts.
Providers, middleware, observers, policies, listeners, events, jobs, and validators
are all auto-discovered and wired by convention. Providers run their full lifecycle
without being listed in bootstrap/providers.ts; middleware registers as a named
group (reference it by class name, or set static global = true); observers and
policies attach by name; listeners bind via static listens; jobs and validators
self-register on import. No manual wiring — see
Conventions.
bootstrap/
bootstrap/app.ts — the application singleton. Wires together exception
handling, global middleware, and opt-in conventions:
// bootstrap/app.ts
import { Application } from "zerotal";
import { DevtoolsInjectionMiddleware } from "@zerotal/devtools";
import { Handler } from "../app/exceptions/Handler.ts";
import providers from "./providers.ts";
export default Application.create({ providers })
.withExceptionHandler(Handler)
.routing({ web: `${import.meta.dir}/../routes/index.ts` })
.fileBasedRouting({ web: `${import.meta.dir}/../app/routes` })
.use([DevtoolsInjectionMiddleware]);
Auth resolves the session user automatically from your registered AuthUser model —
no withUserResolver(...) wiring. Override with app.withUserResolver(...) or
AuthProvider.resolveUsing(...) only for custom logic.
bootstrap/providers.ts — ordered list of provider classes. Registration
order matters: providers lower in the list can depend on bindings from providers
higher up.
Note — Most providers are auto-discovered from
app/providers/. Usebootstrap/providers.tsfor the package providers you register explicitly and when registration order is load-bearing.
config/
Each file exports a typed config object (default export); an optional named
validate(config) export is run at startup. The zt.ts entry point loads the
whole directory synchronously with configLoader("./config") and injects it via
app.useConfig(config.all()). Access at runtime via the Config facade — a static
accessor over a container binding — or typed
per-package helpers. App-wide auto-discovery settings live under the conventions
key of config/app.ts — see Conventions
and the Config system for the full loading flow.
database/
database/migrations/ — migration files named YYYY_MM_DD_HHMMSS_description.ts.
Run with bun zt migrate.
database/seeders/ — seeder classes for populating the database with
test or default data.
database/factories/ — model factories used in tests and seeders.
public/
Files placed here are served directly as static assets. Register the directory
with Router.static('/assets', './public/assets') in routes/index.ts.
routes/index.ts
All explicit route registrations. This file runs after file-based routes are scanned, so explicit routes take precedence over file routes for the same path.
// routes/index.ts
import { Router } from "zerotal";
import { PostController } from "../app/controllers/PostController.ts";
Router.get("/posts", PostController, "index").name("posts.index");
zt.ts
The universal CLI entry point managed by the framework. Do not edit.
# in your project root
bun zt serve # start HTTP server
bun zt worker # start background worker
bun zt migrate # run pending migrations
bun zt route:list # list all routes
bun zt make:model # scaffold a model
bun zt list # all available commands
File-based routing directory
If you call app.fileBasedRouting({ web: "./app/routes" }), that directory mirrors
the URL structure of your app. See the Routing guide for full
details.
# app/routes/ — paths map to URLs
app/routes/
index.ts → GET /
about.ts → GET /about
users/
index.ts → GET /users
[id].ts → GET /users/:id, DELETE /users/:id
[id]/
posts.ts → GET /users/:id/posts
(api)/
_middleware.ts ← middleware for everything below
status.ts → GET /status
Flow Pages directory
Flow Pages have their own directory under app/flow/pages/ (or wherever you
point fileBasedRouting). The directory supports the same (group) and [param]
conventions as file-based routes. A _middleware.ts file in any subdirectory
applies to all Flow Pages below it.
# app/flow/pages/ — Flow routes
app/flow/pages/
(auth)/
_middleware.ts ← GuestMiddleware — only guests see these
login.tsx → /login
register.tsx → /register
(protected)/
_middleware.ts ← AuthMiddleware
dashboard.tsx → /dashboard
settings.tsx → /settings
index.tsx → /
Next steps
- Conventions — how Zerotal wires these directories by name.
- Config system — how the
config/directory is loaded. - Routing — define routes in
routes/index.ts. - Lifecycle — how
bootstrap/app.tsboots the app.