LEVEL 12 · LESSON 3 OF 19

Entering ZudoJS Core

Create the Task API project

Install the zudojs CLI, create the Task API project, run it in development, find your way around its files, try each command, and build it for production.

  • 50 min to read and try
  • You need: "Welcome to ZudoJS" and "Your first Zudo code", Node.js 24 and a working internet connection
  • You build: A ZudoJS Task API that runs in development with npm run dev, publishes its own API docs, and runs in production from compiled JavaScript
Test yourself

BY THE END OF THIS LESSON YOU CAN

  • Install the zudojs CLI, or run it with npx when a global install is not possible
  • Create a project with flags or by answering the CLI's questions
  • Start the server in development and check it with a browser and curl
  • Name the job of each important generated file
  • Build the project and run it for production, and explain what a graceful shutdown does

Install the ZudoJS command-line tool

So far you have installed single ZudoJS packages by hand. A real project needs a dozen of them, a server, a folder layout and scripts. The ZudoJS CLI (command-line interface) sets all of that up with one command.

It is published on npm as zudojs. It gives you two commands that do exactly the same thing: zudojs, and the short zudo. The -g flag installs it globally, so the commands work in any folder:

Terminal on your computer
npm install -g zudojs
added 13 packages in 2s
zudojs --version
2.2.0
zudo --version
2.2.0

The install time depends on your connection. Your version can be higher than 2.2.0. This course writes zudojs, but you can type zudo everywhere instead.

IF THE INSTALL FAILS WITH EACCES

On macOS and Linux, a global install can fail with a permission error, depending on how Node.js was installed. Don't fix it with sudo. Skip the global install and put npx in front instead: npx zudojs@latest --version prints the same version, and npx zudojs@latest create … works for every command in this lesson. The first time, npx asks Ok to proceed? (y); type y.

NOTE

The zudojs package is a thin wrapper around a second package, zudojs-cli, which holds the real code. Installing either one gives you the same commands and the same version. Every framework package is named @zudojs/something. There is no npm package called zudo: that is only a command name.

Create the project

Go to the folder where you keep your projects, not inside ts-tasks, and run:

Terminal on your computer
zudojs create task-api --package-manager npm --capabilities ""
│
◇  Project structure created
│
◇  Backend project generated (43 files)

added 64 packages, and audited 65 packages in 47s

13 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
npm warn install-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn install-scripts   [email protected] (postinstall: node install.js)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.
│
◆  Dependencies installed
│
◇  Project validated
│
◇  Git repository initialized
│
◇  Next steps ──╮
│               │
│  cd task-api  │
│  npm run dev  │
│               │
├───────────────╯
│
└  Project created successfully.

What the options mean:

  • task-api is the name of the project and of the folder it creates.
  • --package-manager npm uses npm, which you already have. The CLI's default is pnpm, a faster alternative you would have to install first.
  • --capabilities "" starts with no optional extras. You will add them one at a time as the course needs them, with zudojs add.

ON WINDOWS POWERSHELL

Windows PowerShell 5 does not pass an empty "" on to programs. Leave --capabilities "" out. When the CLI asks which capabilities to add, press Enter without selecting any.

The CLI wrote the files, installed the packages with npm, checked that the result compiles ("Project validated") and made the folder a git repository. found 0 vulnerabilities means npm audit knows of no security problems in any installed package. The esbuild warning is the same one you met in Why TypeScript exists. It is harmless.

If you leave the options out, the CLI asks you each question in turn instead. The flags simply answer them in advance. The next section shows the questions.

Or answer the questions

If you do not remember a command, run zudojs on its own. In a terminal it opens a numbered menu:

Terminal on your computer
zudojs
┌  zudojs v2.2.0
│
◆  What would you like to do?
│  ● 1. Create a new project
│  ○ 2. Start dev server
│  ○ 3. Generate code
│  ○ 4. Add a feature
│  ○ 5. Build
│  ○ 6. Doctor
│  ○ 7. Info
│  ○ 8. Help
│  ○ 0. Exit
└  ↑/↓ move · 0-8 pick · Enter select · Esc exit

Press a digit to pick an entry, or move with the arrow keys and press Enter. 1 runs zudojs create, 0 exits.

Run zudojs create (or its other name, zudojs new) with no project name, and the CLI becomes interactive: it asks one question at a time. These are the questions, in order. The first choice in each list is selected when the question appears, so pressing Enter takes it:

QuestionChoicesFlag that answers it
What is your project name?Type a name. The grey my-project is only a hint: pressing Enter without typing answers "Project name is required."the name, as in zudojs create task-api
What are you building?Backend, Frontend, Full Stack--type
Select backend architectureMonolith, Modular Monolith, Microservices--architecture
Select databasePostgreSQL, MySQL, SQLite--database
Select API styleREST, GraphQL, RPC--api
Select package managerpnpm, npm, Yarn, Bun--package-manager
Select capabilitiesCQRS, Events, Messaging, Queue, Observability, OpenAPI, Database, Security. Several allowed; none is fine.--capabilities
Create project "…"?Yes / No(asked only in interactive mode)

Keys: ↑ and ↓ move, Enter confirms, and in the capabilities list Space ticks or unticks an item. Each answered question folds into one line. Here it ran with --no-install and --no-git, so it only writes files. After typing my-api, choosing npm with ↓, and pressing Enter for everything else, the terminal shows:

Terminal on your computer
zudojs create --no-install --no-git
┌  Zudojs
│
◇  What is your project name?
│  my-api
│
◇  What are you building?
│  Backend
│
◇  Select backend architecture
│  Monolith
│
◇  Select database
│  PostgreSQL
│
◇  Select API style
│  REST
│
◇  Select package manager
│  npm
│
◇  Select capabilities
│  none
│
◇  Create project "my-api"?
│  Yes
│
◇  Project structure created
│
◇  Backend project generated (42 files)
│
◇  Project validated
│
◇  Next steps ──╮
│               │
│  cd my-api    │
│  npm run dev  │
│               │
├───────────────╯
│
└  Project created successfully.

Keep this my-api folder: it is your practice project, and you will try zudojs generate resource in it below. If you press Ctrl + C during the questions, the CLI prints Operation cancelled. and creates nothing.

Start the server

Terminal on your computer
cd task-api
npm run dev

> [email protected] dev
> tsx watch src/server.ts

2026-09-23T22:38:28.326Z [INFO] [app-service] app service initialized
2026-09-23T22:38:28.328Z [INFO] [task-api] app module initialized
2026-09-23T22:38:28.330Z [INFO] [task-api] All modules initialized. modules=["integrations","app"] durationMs=7
2026-09-23T22:38:28.332Z [INFO] [task-api] All modules started. modules=["integrations","app"] durationMs=1
2026-09-23T22:38:28.334Z [INFO] [task-api] Runtime is ready. runtimeId=rt_7a09dde7083c4b85bfeea8d4cc5471b0 environment=development
Listening on http://0.0.0.0:3000

npm run dev runs the dev script from package.json: tsx watch src/server.ts. That is tsx from Why TypeScript exists, plus watch, which restarts the server every time you save a file.

The lines before the last one are log entries, one per step as the application starts. Each has a time, a level (INFO), which part of the app wrote it in square brackets, and a message. The last line means it is ready: a backend is now listening on your computer, on port 3000. A port is a numbered door on your computer. Each program that listens for network requests uses a different one. 0.0.0.0 means "on every network address of this computer".

Leave it running. Open your web browser and go to http://localhost:3000/health. localhost means "this computer". You will see:

{"status":"ok","checks":{},"timestamp":"2026-09-23T22:38:28.968Z"}

You can do the same from a second terminal window. curl sends a request and prints the answer. -i also prints the status line and headers:

Terminal on your computer
curl -i http://localhost:3000/health
HTTP/1.1 200 OK
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 0
referrer-policy: strict-origin-when-cross-origin
x-dns-prefetch-control: off
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: same-origin
content-security-policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
strict-transport-security: max-age=63072000; includeSubDomains; preload
permissions-policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()
content-length: 66
Date: Wed, 23 Sep 2026 22:38:28 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"status":"ok","checks":{},"timestamp":"2026-09-23T22:38:28.987Z"}
curl http://localhost:3000/tasks
{"error":"Not Found","code":"NOT_FOUND","method":"GET","path":"/tasks"}
curl http://localhost:3000/api/v1/examples
{"items":[],"nextCursor":null}

This is the pattern from Your first Zudo code, now real: a status code (200, 404) and a JSON body. The body of /health says the app is up; checks is empty because nothing like a database is connected yet.

The long list of headers in the middle are security headers. Every response gets them, with no work from you. For example, x-frame-options: DENY stops other websites from showing your pages inside theirs, and x-content-type-options: nosniff stops browsers from guessing a file's type. The security lesson explains each one.

/tasks does not exist yet, so the server answers 404, with a code alongside the message that your own code can check without parsing English text. You build /tasks yourself in Routes, requests and responses. /api/v1/examples is an example resource the CLI wrote so you can see a complete endpoint: it answers a page, items (empty, so far) and a nextCursor for fetching the next page, rather than a bare array, so a list endpoint is never one SELECT * away from returning a million rows.

TIP

On Windows PowerShell, type curl.exe instead of curl. Plain curl there is a different command with different output.

The times, dates and ids you see will be your own. To stop the server, click into its terminal and press Ctrl + C.

REASON IT OUT

Who can reach this server?

The last startup line said Listening on http://0.0.0.0:3000. Before reading on, think it through: who can send requests to your laptop on port 3000 right now? Is that what you want in a café's Wi-Fi? And on a production server behind a load balancer? The /health body says "checks":{}: what should /health answer while the app is still starting, and why would a hosting platform care?

Show the reasoning
  • 0.0.0.0 means every network address of the computer, so anyone on the same network who knows your IP address can call the API, not only you. On a shared Wi-Fi that exposes a half-finished API to strangers. For development, listening on 127.0.0.1 (only this computer) is safer; Configuration changes the default to exactly that.
  • On a production server, 0.0.0.0 is right: the load balancer runs on another machine and must be able to connect.
  • While the app starts or stops, /health should answer 503, not 200. A platform only sends traffic to an instance whose health check passes, so a truthful 503 keeps users away from an app that cannot serve them yet. checks is empty because nothing outside the app, such as a database, is connected yet; once one is, it is listed there.

What the CLI generated

Open the task-api folder in your editor. To see every file at once, tree draws a folder as a tree: -a shows hidden files, --dirsfirst lists folders first, and -I leaves out node_modules and .git, which npm and git manage. (On Windows, or if tree is missing, look in your editor's file list instead.)

Terminal on your computer
tree -a --dirsfirst -I "node_modules|.git"
.
├── src
│   ├── configs
│   │   └── index.ts
│   ├── constants
│   │   └── index.ts
│   ├── controllers
│   │   ├── examples.controller.ts
│   │   └── index.ts
│   ├── databases
│   │   └── index.ts
│   ├── dtos
│   │   ├── examples.dto.ts
│   │   └── index.ts
│   ├── enums
│   │   └── index.ts
│   ├── errors
│   │   └── index.ts
│   ├── events
│   │   └── index.ts
│   ├── integrations
│   │   ├── index.ts
│   │   └── integration.ts
│   ├── interfaces
│   │   └── index.ts
│   ├── jobs
│   │   └── index.ts
│   ├── loaders
│   │   └── index.ts
│   ├── loggers
│   │   └── index.ts
│   ├── middlewares
│   │   └── index.ts
│   ├── models
│   │   └── index.ts
│   ├── modules
│   │   ├── app.module.ts
│   │   └── index.ts
│   ├── repositories
│   │   ├── examples.repository.ts
│   │   └── index.ts
│   ├── routes
│   │   ├── examples.routes.ts
│   │   ├── health.routes.ts
│   │   └── index.ts
│   ├── services
│   │   ├── app.service.ts
│   │   ├── examples.service.ts
│   │   └── index.ts
│   ├── types
│   │   └── index.ts
│   ├── utils
│   │   ├── http.ts
│   │   └── index.ts
│   ├── validators
│   │   └── index.ts
│   ├── app.ts
│   ├── container.ts
│   ├── index.ts
│   └── server.ts
├── tests
│   └── examples.test.ts
├── .zudojs
│   └── manifest.json
├── .env.example
├── .gitignore
├── package.json
├── package-lock.json
├── README.md
├── tsconfig.json
└── tsconfig.test.json

26 directories, 45 files

It looks like a lot, but most folders hold only an empty index.ts: a place ready for one kind of code. The files that do something are these.

The project files

PathWhat it is
package.jsonThe project's name, its 14 @zudojs dependencies, and its scripts: dev, start, build, typecheck and test. Its zudojs block records the project type, the architecture and the features you add.
package-lock.jsonWritten by npm: the exact version of every installed package, so the next install gets the same ones. Commit it; never edit it by hand.
tsconfig.jsonTypeScript settings, like the one you wrote in Why TypeScript exists, with strict on. It compiles src/ into dist/.
tsconfig.test.jsonExtends tsconfig.json to also type-check tests/, without emitting anything: npm run typecheck uses it so a type error in a test fails the same way one in src/ does.
.env.exampleEvery setting the app reads: NODE_ENV, HOST, PORT, CORS_ORIGINS, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX and DATABASE_URL. Copy it to .env to change them. .env is in .gitignore, so real passwords you put there never reach git.
.gitignoreKeeps node_modules/, dist/, .env and log files out of git.
.zudojs/manifest.jsonWhat the CLI knows about the project (monolith, REST, npm, capabilities), so later commands like zudojs generate put files in the right place. Don't edit it by hand.
README.mdHow to start the project, and a map of the folders.

Starting the app

PathWhat it is
src/server.tsThe starting point, run by npm run dev and npm start. It loads the settings, builds a router (the part that picks which code answers which path), puts the security middleware in front of it, starts the runtime, then the HTTP server. On Ctrl + C or SIGTERM it stops them in reverse.
src/app.tscreateApp builds the runtime: the part of ZudoJS that starts and stops every piece of your app in the right order. It gives the runtime a logger, a dependency container, an event bus and the list of modules. Later lessons explain each one.
src/configs/index.tsloadConfig reads the settings from the environment (and .env) and checks them. A wrong PORT stops the app with a clear error instead of starting badly.
src/container.tsThe composition root: it creates every controller, service and repository once, and hands them to the routes. It is the one place where the parts are connected.
src/modules/app.module.tsThe first module, a self-contained piece of the app that the runtime starts and stops. It uses src/services/app.service.ts. Those were the "app service initialized" and "app module initialized" log entries.
src/integrations/integration.ts defines what an integration is: a connection to something outside the app, such as a database or Redis, that starts before your modules, stops after them, and reports its health to /health. The list in index.ts is empty until you zudojs add one.
src/index.tsExports createApp, for tests and tools that want the app without the HTTP server.

Answering requests

PathWhat it is
src/routes/index.tsregisterRoutes adds every route to the router. zudojs generate adds new routes here for you.
src/routes/health.routes.tsAnswers GET /health.
src/routes/examples.routes.tsThe five routes of the example resource at /api/v1/examples: list, get one, create, update, delete.
src/controllers/examples.controller.tsA controller turns a request into a response: it checks the input and calls the service.
src/services/examples.service.tsA service holds the rules, for example "an unknown id is a 404".
src/repositories/examples.repository.tsA repository stores the data. This one keeps it in memory, so it is gone after a restart.
src/dtos/examples.dto.tsThe @zudojs/schema schemas that check what comes in (a DTO, data transfer object, is the shape of data that crosses the API).
src/utils/http.tsSmall helpers: json builds a response, readJsonBody reads a request body, validationFailed answers 400, and securityHeaders adds the security headers.
tests/examples.test.tsThree tests for the example resource, so npm test works from day one.

The controller, service and repository are the layers from Backend architecture. The example resource shows them working together; you can copy its shape, and delete it when you no longer need it. The empty folders (constants/, databases/, enums/, errors/, events/, interfaces/, jobs/, loaders/, loggers/, middlewares/, models/, types/, validators/) are places for code you write later, so every ZudoJS project keeps each kind of file in the same spot.

Here is the /health route. router.get says "when a GET request for /health arrives, run this function". json is a small helper from src/utils/http.ts that builds a response with a status code and a JSON body:

src/routes/health.routes.tsNode.js only
import type { HttpRouter } from "@zudojs/http";

import { json } from "../utils/http.js";

/** What /health reports. */
export interface HealthReport {
  readonly ready: boolean;
  readonly checks: Readonly<Record<string, "up" | "down">>;
}

/** Computes the current {@link HealthReport}. */
export type HealthCheck = () => Promise<HealthReport>;

export function registerHealthRoutes(router: HttpRouter, check: HealthCheck): void {
  router.get(
    "/health",
    async () => {
      const report = await check();
      return json(report.ready ? 200 : 503, {
        status: report.ready ? "ok" : "unavailable",
        checks: report.checks,
        timestamp: new Date().toISOString(),
      });
    },
    { openapi: false },
  );
}

It answers 503 Service Unavailable while the app is starting or stopping, or when a connected service is down. A hosting platform reads that and sends no traffic until the app is ready. { openapi: false } hides this route from the API documentation you will add below.

You can try the same idea on its own, without starting a server. createHttpTestClient comes from @zudojs/testing, which the project already has for its tests. It sends a request straight to a router and gives you the answer:

health-check.tsNode.js only
import { createResponseContext, createRouter } from "@zudojs/http";
import { createHttpTestClient } from "@zudojs/testing";

let ready = false;
const router = createRouter();
router.get("/health", async () =>
  createResponseContext({ status: ready ? 200 : 503 }).json({ status: ready ? "ok" : "unavailable" }),
);

const client = createHttpTestClient(router);
const starting = await client.get("/health");
console.log(starting.status, starting.json());

ready = true;
const running = await client.get("/health");
console.log(running.status, running.json());
await client.close();
Output of npx tsx health-check.ts
503 { status: 'unavailable' }
200 { status: 'ok' }

Same route, two answers: 503 while the app is not ready, 200 once it is.

And here is the part of src/server.ts that every request goes through. It is shortened; open the file to see all of it:

server.ts (part)Node.js only
const router = createRouter();
registerRoutes(router, createDependencies({ health: /* … */ }));

const pipeline = new HttpMiddlewarePipeline({
  middlewares: [
    securityHeaders(),
    createCorsMiddleware({ allowOrigin: config.corsOrigins }),
    createRateLimitMiddleware({ windowMs: config.rateLimit.windowMs, max: config.rateLimit.max }),
    dispatch,
  ],
});

A request passes through each middleware in the list, in order, before dispatch hands it to the router:

  • securityHeaders() adds the security headers you saw in the curl output.
  • createCorsMiddleware decides which other websites may call your API from a browser. The list comes from CORS_ORIGINS, which is empty, so the answer is "none". That is the safe default: you open the door on purpose, one website at a time.
  • createRateLimitMiddleware allows each client 1000 requests per minute (RATE_LIMIT_MAX, RATE_LIMIT_WINDOW_MS). One more gets 429 Too Many Requests with a retry-after header, so a single client cannot flood your server.

The middleware lesson shows how to write your own.

Check the project

The project comes with the same check-then-run habits you learned in Why TypeScript exists, as npm scripts:

Terminal on your computer
npm run typecheck

> [email protected] typecheck
> tsc --noEmit

npm test

> [email protected] test
> vitest run


 RUN  v5.0.1 ~/task-api


 Test Files  1 passed (1)
      Tests  4 passed (4)
   Start at  23:38:38
   Duration  1.97s (import 81%, transform 13%, tests 6%, worker 1%)

zudojs doctor
Zudojs Doctor - Project Diagnostics

✔ Node.js version: Node.js v24.19.0 (meets minimum v24)
✔ Git: git version 2.53.0
✔ Package manager (npm): 11.19.0
✔ Zudojs project: backend (monolith) from .zudojs/manifest.json
✔ Package manager: npm (lock file present)
✔ Dependencies installed: node_modules present
✔ TypeScript configuration: tsconfig.json found in every app
✔ Zudojs dependencies: 14 Zudojs package(s) declared
✔ Features: Every declared feature has its package
✔ Capabilities: The manifest and package.json record the same capabilities

All checks passed!
  • npm run typecheck is tsc --noEmit. No output means no type errors.
  • npm test runs the tests with Vitest. The four tests create, read, update and delete an example, list a page of them, and check that an invalid body and an id that is not a UUID each get a 400. You will write your own tests later in the course.
  • zudojs doctor checks your whole setup: Node.js version, dependencies, configuration. Run it first whenever something seems wrong.

Every CLI command

You have used create and doctor. zudojs --help lists all seven. This section tries each one once; the next lesson, The ZudoJS CLI in depth, covers every option, the naming and wiring rules, and the errors. Run it inside task-api:

Terminal on your computer
zudojs --help
zudojs v2.2.0
Command-line interface for the Zudojs framework.

Usage:
  zudojs <command> [options]

Commands:
  add           Add a feature package to a Zudojs project
  build (b)     Build a Zudojs project
  create (new)  Create a new Zudojs project
  dev (d)       Start development servers
  doctor        Run diagnostics on a Zudojs project
  generate (g)  Generate files within a Zudojs project
  info          Show information about the Zudojs CLI and project

Options:
  -h, --help     Show help.
  -v, --version  Show version.

Run "zudojs help <command>" for help on one command.

The words in brackets are other names: zudojs g is zudojs generate. Every command has its own help, for example zudojs help create or zudojs generate --help. Here is what each one is for:

CommandWhat it doesUseful options
zudojs create [name]Creates a project. Asks questions for anything you do not pass as a flag.--architecture, --package-manager, --database, --capabilities, --no-install, --no-git
zudojs devStarts the development server. For this project it runs npm run dev.-p 4000 picks the port
zudojs buildCompiles the project for production. For this project it runs npm run build.
zudojs generate <schematic> <name>Writes new files from a template into the right folders. Schematics: resource, service, module, command, query, controller, repository, middleware, event, job, route, model, dto, validator.--dry-run shows the files without writing them; --force overwrites
zudojs add <feature>Adds a feature to the project: writes the code, registers it, adds its settings and installs its package. Features: database, redis, websockets, email, docker, queue, messaging, openapi, observability, cache, storage, scheduler.--skip-install
zudojs doctorChecks Node.js, the package manager, dependencies and the project files.
zudojs infoPrints the CLI version and the project's type, architecture and ZudoJS packages.

info is the quickest way to see what a project is made of:

Terminal on your computer
zudojs info
Zudojs CLI
  Version: 2.2.0
  Node.js: v24.19.0

Project
  Name: task-api
  Version: 0.1.0
  Type: backend
  Architecture: monolith
  Package manager: npm
  Capabilities: (none)

Zudojs dependencies
  @zudojs/config: ^1.3.3
  @zudojs/constants: ^1.1.4
  @zudojs/container: ^1.2.3
  @zudojs/core: ^1.2.4
  @zudojs/errors: ^1.3.2
  @zudojs/events: ^1.3.3
  @zudojs/http: ^1.4.4
  @zudojs/logger: ^1.4.3
  @zudojs/runtime: ^1.3.3
  @zudojs/schema: ^1.2.3
  @zudojs/security: ^1.3.3
  @zudojs/types: ^1.2.0
  @zudojs/validation: ^1.1.2

The ^ in front of each version means "this version or any later one without breaking changes" (npm and packages explained it). Your numbers can be higher.

Generate code

generate with --dry-run tells you what it would write, and writes nothing. It is a safe way to learn where each kind of file belongs. The most useful schematic is resource: a complete endpoint with every layer:

Terminal on your computer
zudojs generate resource tasks --dry-run
Detected architecture: monolith
Dry run: 8 files would be written or updated (nothing written):
  - src/dtos/tasks.dto.ts
  - src/repositories/tasks.repository.ts
  - src/services/tasks.service.ts
  - src/controllers/tasks.controller.ts
  - src/routes/tasks.routes.ts
  - tests/tasks.test.ts
  - src/routes/index.ts
  - src/container.ts
zudojs generate middleware audit --dry-run
Detected architecture: monolith
Dry run: 3 files would be generated (nothing written):
  - src/middlewares/audit.middleware.ts
  - src/middlewares/index.ts
  - src/server.ts

The CLI read .zudojs/manifest.json, saw a monolith, and chose the folders to match. In a modular monolith the same command would place the files inside a module. The last two lines of the first list are files it would update: it adds the new routes to registerRoutes and the new controller to src/container.ts, so the endpoint works with no wiring by hand. The middleware works the same way: besides audit.middleware.ts, it would export the new function from src/middlewares/index.ts and add auditMiddleware() to the list of middleware in src/server.ts, between the // zudojs:server-middleware markers.

Do not run that one for real in task-api: later in this course you build /tasks yourself, so you understand every line. Try it for real in your practice project instead:

Terminal on your computer
cd my-api
npm install
…
zudojs generate resource notes
Detected architecture: monolith
Generated 8 files:
  - src/dtos/notes.dto.ts
  - src/repositories/notes.repository.ts
  - src/services/notes.service.ts
  - src/controllers/notes.controller.ts
  - src/routes/notes.routes.ts
  - tests/notes.test.ts
  - src/routes/index.ts
  - src/container.ts
npm test

> [email protected] test
> vitest run


 RUN  v5.0.1 ~/my-api


 Test Files  2 passed (2)
      Tests  8 passed (8)
   Start at  23:39:26
   Duration  2.12s (import 85%, transform 8%, tests 6%)

The new tests pass straight away. Start it with PORT=3001 npm run dev (port 3001, so it does not clash with task-api), and in a second terminal create a note, list the notes, then send two bad requests:

Terminal on your computer
curl -i -X POST http://localhost:3001/api/v1/notes -H "content-type: application/json" -d '{"name":"Buy milk"}'
HTTP/1.1 201 Created
content-type: application/json
…
{"id":"e4f62fa7-5b17-4064-b267-9febf15dae0c","name":"Buy milk","createdAt":"2026-09-23T22:39:40.302Z","updatedAt":"2026-09-23T22:39:40.302Z"}
curl http://localhost:3001/api/v1/notes
{"items":[{"id":"e4f62fa7-5b17-4064-b267-9febf15dae0c","name":"Buy milk","createdAt":"2026-09-23T22:39:40.302Z","updatedAt":"2026-09-23T22:39:40.302Z"}],"nextCursor":null}
curl -X POST http://localhost:3001/api/v1/notes -H "content-type: application/json" -d '{"name":""}'
{"error":"Validation failed","issues":[{"path":"name","message":"String must be at least 1 character"}]}
curl http://localhost:3001/api/v1/notes/123
{"error":"Validation failed","issues":[{"path":"id","message":"Invalid uuid format"}]}

One command gave you a working endpoint that checks its input: an empty name and an id that is not a UUID (the long random id format) are both refused with a 400. The list came back as items plus a nextCursor, the same paginated shape as /api/v1/examples, rather than a bare array. The notes live in memory, so they are gone when the server restarts. On Windows PowerShell, set the port with $env:PORT = "3001" first, and put the JSON in a file with -d "@note.json", because PowerShell handles the quotes differently.

Every schematic writes code that compiles as it is. generate command and generate query are for the CQRS lesson: they need @zudojs/cqrs, which a new project does not have yet. The CLI adds it to package.json and tells you to install it:

Terminal on your computer
zudojs generate command create-note
Detected architecture: monolith
Generated 3 files:
  - src/commands/create-note/create-note.command.ts
  - src/commands/create-note/create-note.handler.ts
  - src/commands/create-note/index.ts
Warning: Added @zudojs/cqrs to package.json.
Warning: Run your package manager's install command to fetch them.
npm install

added 1 package, and audited 66 packages in 2s
…
npm run typecheck

> [email protected] typecheck
> tsc --noEmit

Until you run npm install, npm run typecheck fails with "Cannot find module '@zudojs/cqrs'". The three files hold the command, a handler with an execute method for your logic, and a registerCreateNoteCommand function that puts the handler on a command bus. The CQRS lesson explains commands, handlers and buses.

Add a feature: API docs

add plugs a feature into the project. It writes the code, wires it in and installs the package. Try it with openapi, which publishes a description of your API. Stop the server first, then run this inside task-api:

Terminal on your computer
zudojs add openapi
Adding feature: openapi — OpenAPI document at /openapi.json and a docs page at /docs
Updated:
  - src/server.ts
  - package.json
Installing dependencies with npm...

up to date, audited 65 packages in 2s

13 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
npm warn install-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn install-scripts   [email protected] (postinstall: node install.js)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.
Feature "openapi" added successfully.
Next: Describe routes with the `openapi` route option; hide one with `openapi: false`.

up to date means npm had nothing new to download: the package it needs came along with @zudojs/http. The CLI added one line to src/server.ts, mountOpenAPI(router, …), and recorded "openapi" in the project's features. Start the server again with npm run dev:

Terminal on your computer
curl -s http://localhost:3000/openapi.json | head -4
{
  "openapi": "3.1.0",
  "info": {
    "title": "task-api",

OpenAPI is a standard format that describes every route of an API: its path, what it accepts and what it answers. Other tools read it to test your API or to write client code for it. Every route with an openapi option ends up in this document, so the examples routes are listed and /health is not.

Now open http://localhost:3000/docs in your browser. It is an interactive page built from that document: click a route, then Try it out, and you can send a real request to your running server. The OpenAPI lesson goes further.

Other features write more. zudojs add database, for example, writes a database connection in src/integrations/, adds DATABASE_URL to .env.example and new scripts to package.json. You will use it in Databases with @zudojs/database.

Build and run for production

npm run dev is for your own computer: tsx compiles TypeScript on the fly and restarts on every save. A production server should not do that work. There, you compile once to plain JavaScript, and Node.js runs the result. That step is called the build:

Terminal on your computer
npm run build

> [email protected] build
> tsc

No output means no errors. build is plain tsc: this project's tsconfig.json has "outDir": "dist" and "rootDir": "src", so every src/ file now has a JavaScript twin in dist/, such as dist/server.js for src/server.ts. zudojs build does the same through the CLI:

Terminal on your computer
zudojs build
Building project at: ~/task-api

> [email protected] build
> tsc

Build completed successfully.

Now start the compiled app. The start script is node dist/server.js: no tsx, no watching. NODE_ENV=production is the usual way to tell a Node.js app it runs in production:

Terminal on your computer
NODE_ENV=production npm start

> [email protected] start
> node dist/server.js

2026-09-23T22:41:13.395Z [INFO] [app-service] app service initialized
2026-09-23T22:41:13.398Z [INFO] [task-api] app module initialized
2026-09-23T22:41:13.399Z [INFO] [task-api] All modules initialized. modules=["integrations","app"] durationMs=7
2026-09-23T22:41:13.401Z [INFO] [task-api] All modules started. modules=["integrations","app"] durationMs=1
2026-09-23T22:41:13.403Z [INFO] [task-api] Runtime is ready. runtimeId=rt_fd982c8347c3402288c2a47a8aecce2e environment=production
Listening on http://0.0.0.0:3000

The runtime noticed environment=production. curl http://localhost:3000/health answers exactly as before, and so does /openapi.json, which you added above — but /docs now answers 404: the interactive page is a development convenience, off by default once NODE_ENV=production, so an internal API description is not one URL away from anyone who finds your server. Pass docsPath to mountOpenAPI to serve it in production anyway, behind auth if it is not meant to be public. Now press Ctrl + C and read what the app prints on its way out:

Terminal on your computer
^CReceived SIGINT: shutting down.
2026-09-23T22:41:14.343Z [INFO] [task-api] Initiating graceful shutdown. timeoutMs=30000
2026-09-23T22:41:14.345Z [INFO] [task-api] app module stopped
2026-09-23T22:41:14.345Z [INFO] [task-api] All modules stopped. modules=["app","integrations"] durationMs=1
2026-09-23T22:41:14.346Z [INFO] [task-api] All modules destroyed. durationMs=0
2026-09-23T22:41:14.347Z [INFO] [task-api] Graceful shutdown complete.
2026-09-23T22:41:14.347Z [INFO] [task-api] Runtime stopped. runtimeId=rt_fd982c8347c3402288c2a47a8aecce2e

This is the graceful shutdown the BookStore never had (the honest review listed it). The first line is src/server.ts noticing the signal. The server stopped taking requests, then the runtime stopped every module in reverse order (app first, integrations last), with a 30-second limit in case something hangs. A hosting platform sends SIGTERM instead of Ctrl + C when it replaces your app, and src/server.ts handles both the same way.

TIP

On Windows PowerShell, set the variable first with $env:NODE_ENV = "production", then run npm start.

The generated .gitignore already lists dist/: build output is made from the source, so it never belongs in git. The deployment lesson runs exactly these two steps, npm run build and npm start, inside a Docker image.

Practice

TRY IT YOURSELF

Change the health response

With npm run dev running, open src/routes/health.routes.ts and add a service: "task-api" property to the object it sends. Save the file. Watch the terminal restart the server, then reload http://localhost:3000/health.

Write it in the editor and run it. Hints and the solution open up once you have tried.

TRY IT YOURSELF

Where does a repository go?

Without writing anything, find out where zudojs generate would put a repository called tasks.

Work it out first, on paper or in your head. Then use the hints, and compare with the solution.

TRY IT YOURSELF

Development or production?

You changed src/routes/health.routes.ts, then ran npm start. The old answer comes back. Why, and what is the fix?

Work it out first, on paper or in your head. Then use the hints, and compare with the solution.

TRY IT YOURSELF

Let one website in

Your API gets a web front end at https://tasks.example.com. Right now the browser blocks it, because CORS_ORIGINS is empty. How do you allow that one website, without allowing every website?

Work it out first, on paper or in your head. Then use the hints, and compare with the solution.

Recap

  • npm install -g zudojs gives you the zudojs command and its short name zudo. npx zudojs@latest works without a global install.
  • zudojs create task-api --package-manager npm creates a project with a server, a router, a runtime, security defaults, an example resource and tests, installs its packages and sets up git.
  • zudojs on its own opens a menu; zudojs create with no options asks its questions one at a time.
  • npm run dev starts the server and restarts it when you save. /health answers 200 when the app is ready; every response carries security headers, CORS is closed and each client is rate limited.
  • npm run typecheck, npm test and zudojs doctor keep the project healthy.
  • The CLI has seven commands: create, dev, build, generate, add, doctor and info. generate resource writes a whole endpoint and registers it; --dry-run shows what would be written. add openapi serves /openapi.json and a docs page at /docs.
  • For production, npm run build compiles src/ into dist/, and NODE_ENV=production npm start runs it with plain Node.js. Ctrl + C or SIGTERM shuts it down gracefully.

You have a running ZudoJS backend. Before you give it real routes, using the TaskService and schema you wrote in Your first Zudo code, learn the tool that made it. Next, The ZudoJS CLI in depth.

Test yourself

Five questions, picked at random from this lesson's question bank. Some ask you to choose an answer, some to predict what code prints, and some to write code and run it in the terminal. Get 4 of 5 right to pass. If you don't, read the explanations and try again: you get 5 different questions.