Scaffold an Express 5 + TypeScript backend: database, auth, optional Next.js front end.
A lightweight, decorator-driven Express + TypeScript backend starter. Clone, run, ship.
π Documentation Β· Getting started Β· create-chassis on npm
Chassis gives you NestJS-style controller ergonomics on plain Express 5 β in a handful of small files you can actually read. Zero configuration required: the server boots standalone, and every integration switches on only when you add its environment variable. Scaffold with a preset or pick Γ la carte β a database (Mongo, Postgres, or SQLite, ORM included), an auth provider (Auth0, Clerk, or built-in local sign-in), an optional Next.js front end, Sentry, an MCP server, and x402 payments β and the CLI ships only what you chose.
export class UserController extends Routable {
constructor() {
super('/users');
}
@route('get', '/:id')
async show(req: Request) {
const user = await findUser(req.params.id);
if (!user) throw new AppError(ERROR_CODES.NOT_FOUND, 'User not found');
return req.resHandler.ok(user);
}
@protectedRoute('post', '/', [validate({ body: createUserSchema })])
async create(req: Request) {
return req.resHandler.created(await createUser(req.body));
}
}
Export the class from src/controllers/index.ts β that's the whole wiring.
npm create chassis my-api -- --yes # zero prompts: Postgres + JWT + Sentry + Docker
npm create chassis my-app -- --preset fullstack --yes # the same, plus a Next.js front end
npm create chassis my-api # interactive β pick a preset
npm create chassis my-api -- --db postgres --auth jwt --mcp # Γ la carte
npm create chassis my-api -- --bare # nothing β standalone build
Or use the template directly:
git clone https://github.com/dvd90/chassis.git my-api
cd my-api && npm install && npm run dev
That's it β no database, no env file, no accounts needed. Open http://localhost:8000/status.
New here? Follow the step-by-step getting-started guide β zero to a tested API in ~10 minutes.
Every path is non-interactive: --yes and --bare never prompt, and the CLI
skips prompts automatically whenever stdin isn't a TTY. One command produces a
project that already typechecks, lints and tests green.
Generated projects carry AGENTS.md, CLAUDE.md, llms.txt and an
add-resource skill, so whichever agent opens one writes code that matches the
rest of the codebase rather than fighting it.
@route / @protectedRoute on controller methods, controllers auto-mountreq.resHandler.ok() / .notFound() / .validation() with structured loggingcallId (or propagates x-call-id), echoed in responses and logssrc/config; the app refuses to boot on bad configvalidate({ body, query, params }) middleware with structured 400spackage.json carries only what you chose@paidRoute('get', '/report', '$0.01') via the x402 protocol (opt-in)--web adds an App Router app and makes the project an npm-workspaces monorepo (apps/api + apps/web); the auth provider you picked is wired on both sidesnpm run mcp, opt-in)/healthz (liveness) and /readyz (readiness, checks enabled integrations)npm run gen user scaffolds a controller + test wired to your ORM (Drizzle or Mongoose)AGENTS.md, CLAUDE.md, llms.txt, and an add-resource skill so agents write code that matches the conventions (see below)Most people scaffolding a backend today have an AI agent in the loop. Chassis is built so that agent-written code reads like hand-written code β because the framework gives agents rails and a verifiable finish line:
AGENTS.md + CLAUDE.md ship in every project β Claude Code, Cursor, Copilot, and Codex pick them up automatically and follow the conventions (thin controllers, resHandler responses, throw AppError, config in one place).npm run verify (strict TypeScript + ESLint + tests) is a deterministic quality gate agents iterate against until green..claude/skills/add-resource turns "add a books resource" into one consistent, checklisted operation.llms.txt gives doc-fetching tools a compact map of the conventions.Nothing to install β it's all in the scaffold. See AGENTS.md.
| Command | What it does |
|---|---|
npm run dev | Start with hot reload (tsx watch) |
npm test / npm run test:watch | Run the vitest suite |
npm run verify | Typecheck + lint + test (CI runs this) |
npm run build / npm start | Compile to dist/ and run production build |
npm run gen <Name> | Generate a controller + test |
npm run lint / npm run format | ESLint / Prettier |
Copy .env.example to .env. Each integration turns on when its variables are set β and stays completely dormant otherwise:
| Integration | Enable by setting | What you get |
|---|---|---|
| MongoDB | MONGODB_URI | Mongoose connection, readiness check, graceful disconnect |
| Auth0 | AUTH0_DOMAIN + AUTH0_AUDIENCE | JWT verification on every @protectedRoute |
| Sentry | SENTRY_DSN | Automatic error reporting from the central error handler |
Using a different IdP? Call setAuthProvider([...yourMiddleware]) at boot and @protectedRoute uses it β see src/core/auth.ts.
Local sign-in ships in three variants β emailed link, the classic credential
form, or both. Run npm create chassis --help to see the --auth values, or
read Authentication. Whichever you pick, they
share one session layer.
POST /auth/magic/request {email, returnTo?} β 202, identical for every address
GET /auth/magic/:token β confirm page β consumes nothing
POST /auth/magic/redeem {token} β session + redirect
POST /auth/magic/code {email, code} β same, from the other device
POST /auth/refresh | /auth/logout | /auth/revoke-all
Four things worth knowing about the emailed-link flow:
GET never spends a token. Mail security scanners prefetch links, and a
single-use token burned by a scanner is how this feature usually breaks in
production. Redemption is a POST, on a click.SESSION_IDLE, hard SESSION_ABSOLUTE cap.| Variable | Default |
|---|---|
JWT_SECRET | (required) |
SESSION_IDLE / SESSION_ABSOLUTE | 30d / 90d |
MAGIC_TOKEN_TTL / MAGIC_CODE_ATTEMPTS | 15m / 5 |
MAGIC_LINK_BASE_URL | http://localhost:8000 |
SMTP_URL | unset β logs the email |
Chassis binds no email or SMS provider β bind yours through setMailTransport()
or setSmsTransport(). Proving an address fires one hook, setOnVerified(),
and that is the whole extension surface: consent and onboarding are yours.
Guides: magic link Β· sessions Β· transports
src/
βββ config/ # zod-validated env β typed config + feature flags
βββ core/ # the framework: Routable, decorators, responses, errors, validation
βββ middleware/ # callId correlation, dev request logging
βββ integrations/ # opt-in modules: mongo, auth0, sentry
βββ controllers/ # your endpoints β exported classes auto-mount
βββ __tests__/ # vitest + supertest
βββ app.ts # pure app factory (no I/O β trivially testable)
βββ server.ts # boot: integrations β listen β graceful shutdown
Read them at dvd90.github.io/chassis β
searchable, one page. The source lives in docs/ and the site
is generated from it, so the two can never disagree:
docker compose up --build # API + MongoDB
docker build -t my-api . # production image only
Source-derived launch command. Check the maintainerβs required arguments and credentials before running:
npx -y chassis-mcpMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-dvd90-chassis-mcp": {
"command": "npx",
"args": [
"-y",
"chassis-mcp"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the clientβs tool list, then try a read-only example from its documentation.
Claude Desktop setup referencechassis-mcpnpmChassis works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.