LEVEL 12 · LESSON 1 OF 19

Entering ZudoJS Core

Welcome to ZudoJS

Meet ZudoJS: its 38 packages and how they are layered, the zudojs CLI and three application shapes. Then run three of its packages together in your browser.

  • 25 min to read and try
  • You need: "What a framework does" and "Backend architecture" from the Software design and architecture course
  • You build: A book service that uses three ZudoJS packages together
Test yourself

BY THE END OF THIS LESSON YOU CAN

  • Explain what ZudoJS is and why it is split into small packages
  • Use the shelf rule to say which packages may depend on which
  • Name the package that owns a given job, such as background jobs, permissions or settings
  • Follow one request through the packages that handle it
  • Choose between a monolith, a modular monolith and microservices for a given team and app

What ZudoJS is

ZudoJS is a backend framework for TypeScript on Node.js. It takes over the jobs you listed in What a framework does: lifecycle, dependency injection, routing, configuration, validation, errors, data access, security, testing and project structure.

It is not one big package. It is 38 small npm packages, each named @zudojs/something, plus a command-line tool published as zudojs. Each package owns one job. You install only the ones you use, and you can use most of them on their own, even in a project that is not a ZudoJS app.

That design follows the ideas from Backend architecture: separation of concerns, interfaces between parts, dependency injection, and dependencies that point in one direction only.

Packages on shelves

Think of the packages as sitting on shelves. A package may use packages on lower shelves, never higher ones. So there are no cycles, and each package can be built, tested and understood on its own.

  top      testing                   fakes and helpers for all the others
           http, cli                 talk to the outside world
           runtime, auth, cqrs, …    put the parts together into an application
           core, events, schema, …   framework machinery
           config, logger, …         single-purpose building blocks
  bottom   errors, types             depend on no other ZudoJS package
  • At the bottom is @zudojs/errors. Almost every other package uses it, so an error from any package is a BaseError with a status code and a stable code. @zudojs/types sits beside it with shared type guards.
  • In the middle are building blocks such as @zudojs/config, @zudojs/logger, @zudojs/container and @zudojs/schema. For example, @zudojs/schema uses only errors, constants and types.
  • Higher up are the packages that combine others: @zudojs/runtime starts and stops an application, and @zudojs/http serves it on the network.

The Architecture and Dependency direction pages list every package's exact dependencies.

The packages, grouped by job

You do not need to remember this table. Come back to it when a lesson mentions a package. Each name links to its documentation.

JobPackages
Foundations: errors, shared types and constants, checking dataerrors, types, constants, schema, validation, serialization
Application core: modules, start and stop, dependency injection, settings, logscore, runtime, lifecycle, container, config, logger, plugins
HTTP and APIs: serving requests, and describing and calling APIshttp, middleware, api, rpc, openapi, adapters
Data: databases, storage, transactions, cachingdatabase, storage, transactions, cache
Users and security: log-in, permissions, protection, cryptographyauth, auth-oauth, permissions, security, crypto, tenancy
Events and background workevents, messaging, cqrs, queue, scheduler
Running in production: seeing inside, switching features, testingobservability, feature-flags, testing
Tools: creating projects and documentationzudojs (the CLI), docs

A normal application starts with about a dozen of them. The rest arrive when you need them: queue when you have background jobs, tenancy when you serve several customers from one app, and so on. The package list has a search box.

How the packages fit together

Here is the path one request takes through a ZudoJS application, with the package that handles each step. Compare it with the BookStore, where you wrote every step yourself:

StepPackageBookStore equivalent
Start every part in order, stop in reverse on SIGTERMruntime, core(missing)
Read and check the settingsconfigconfig.ts
Accept the request, run middlewarehttp, securityapp.ts, router.ts, json.ts
Find out who is asking, and whether they mayauth, permissionsrequireUserId
Check the bodyschemavalidation/
Hand the controller its servicecontainercreateRouter
Load and save datadatabase, transactionsrepositories/, db.ts
Turn a failure into a status codeerrorserrors.ts, toErrorResponse
Write what happenedlogger, observabilityconsole.log

The packages that do not need Node.js also run in this page's browser terminal. Here are three of them working together: @zudojs/schema checks the input, @zudojs/errors reports failures, and @zudojs/container hands out the one shared service:

books.ts
import { ContainerScope, createContainer, createToken } from "@zudojs/container";
import { BaseError, NotFoundError } from "@zudojs/errors";
import { schema } from "@zudojs/schema";
import type { Infer } from "@zudojs/schema";

const NewBookSchema = schema.object({
  title: schema.string().trim().min(1).max(200),
  priceCents: schema.number().int().min(0),
});
type Book = Infer<typeof NewBookSchema> & { readonly id: number };

class BookService {
  private readonly books: Book[] = [];

  add(input: unknown): Book {
    const book = { id: this.books.length + 1, ...NewBookSchema.parse(input) };
    this.books.push(book);
    return book;
  }

  get(id: number): Book {
    const book = this.books.find((b) => b.id === id);
    if (book === undefined) throw new NotFoundError(`Book ${id} not found`);
    return book;
  }
}

const BOOKS = createToken<BookService>("books");
const container = createContainer();
container.register(BOOKS, { useFactory: () => new BookService() }, { scope: ContainerScope.SINGLETON });

const books = container.resolve(BOOKS);
console.log(books.add({ title: "  Kindred ", priceCents: 1099 }));
console.log("same service everywhere:", container.resolve(BOOKS) === books);

for (const attempt of [() => books.add({ title: "", priceCents: -1 }), () => books.get(9)]) {
  try {
    attempt();
  } catch (error) {
    if (error instanceof BaseError) console.log(error.name, error.statusCode, error.code, error.message);
  }
}
Output of npx tsx books.ts and of the browser terminal
{ id: 1, title: 'Kindred', priceCents: 1099 }
same service everywhere: true
SchemaError 400 ERR_SCHEMA_VALIDATION Validation failed
NotFoundError 404 ERR_RESOURCE_NOT_FOUND Book 9 not found

Press Run in browser and change things. What to notice:

  • One description, two uses. NewBookSchema checks the data at runtime, and Infer turns it into the Book type. In the BookStore you wrote the interface and the parser separately.
  • The container creates BookService the first time someone asks for the BOOKS token, and gives everyone that same instance (SINGLETON). No createRouter threading objects through by hand.
  • One error family. The SchemaError thrown by @zudojs/schema and the NotFoundError you threw both extend BaseError from @zudojs/errors, so one instanceof check reads the status code of either.

The next lesson installs @zudojs/schema and @zudojs/errors on your computer and explains them in detail. The container gets its own lesson later in this course, Dependency injection with @zudojs/container.

The command-line tool

Choosing a dozen packages and wiring them into a project by hand would bring back the problem you just left behind. So ZudoJS has a CLI (command-line interface), published on npm as zudojs. It gives you a command called zudojs, and a short name for it, zudo. Run on its own, it shows a menu of these commands:

CommandWhat it does
zudojs createCreates a new project: folders, packages, scripts, a server with security defaults, an example endpoint and tests.
zudojs devStarts the development server, restarting when you save.
zudojs buildCompiles the project to JavaScript for production.
zudojs generateAdds a complete endpoint (generate resource), or a single service, controller, module and more, in the right folder, and wires it in.
zudojs addAdds a feature such as database, redis or docker to an existing project: the code, the settings and the package.
zudojs doctorChecks your Node.js, packages and project for problems.
zudojs infoShows the CLI version and the project's ZudoJS packages.

You will install it and try every one of these in Create the Task API project, and study each command's options and rules in The ZudoJS CLI in depth.

Three shapes of application

zudojs create asks which architecture you want. There are three, and they differ in how many deployable programs you end up with:

ArchitectureWhat it isChoose it when
MonolithOne application, one codebase, one process. Like the BookStore.You are starting out, or the team is small. This is the default and what this course uses.
Modular monolithStill one deployable application, but split into strict modules (say, catalog, orders, users) that talk through clear interfaces instead of reaching into each other's code.The app has grown and you want clear boundaries, without the cost of running many services.
MicroservicesSeveral separate applications, each with its own data, talking over the network, usually behind a gateway.Different parts must scale or be deployed independently, by different teams. It adds a lot of operational work.

A common and healthy path is monolith → modular monolith → microservices only where needed. The same ZudoJS packages work in all three, and the ZudoJS architecture course walks that path, from A well-structured monolith to From monolith to modular monolith and beyond.

REASON IT OUT

Which shape for this team?

Before reading the answer, pick a shape for each case, and say what would go wrong with the other two:

  • Two developers start a booking app for a chain of three hotels.
  • Four years later, twenty developers in four teams work on bookings, payments, housekeeping and reporting in one codebase. Every release needs all four teams to agree.
  • The reporting part runs heavy queries every night that slow bookings down, and it must scale on its own.
Show the reasoning
  • Two developers: a monolith. One process is the easiest to run, debug and deploy. Microservices would give two people the network failures, separate deployments and data copying of a large company, with none of the benefits.
  • Four teams in one codebase: a modular monolith. The pain is unclear boundaries, not scaling. Strict modules with clear interfaces let each team own its part while the app stays one deployment. Jumping straight to microservices would turn every function call between teams into a network call.
  • Reporting that must scale alone: now one piece has a real reason to be separate. Split that module out as a service, and keep the rest together. Microservices where they pay off, not everywhere.

Practice

TRY IT YOURSELF

Which package?

For each need, name the ZudoJS package: (a) send a welcome e-mail in the background after sign-up; (b) let only admins delete books; (c) read DATABASE_URL and fail if it is missing; (d) count requests per second on a dashboard.

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

TRY IT YOURSELF

Look up a missing book

Change the example so that BookService also has findByTitle(title), which throws NotFoundError when no book has that exact title. Try it with a title that exists and one that does not.

Write it in the editor, then press Check. Hints and the solution open up once you have checked your code.

Recap

  • ZudoJS is 38 small @zudojs/* packages plus the zudojs CLI. Each owns one job, and you install only what you use.
  • Packages sit on shelves and only use packages below them. @zudojs/errors is at the bottom, so every error shares one base class.
  • A request passes through http, security, auth, schema, container, database and errors, and runtime starts and stops it all.
  • The zudojs CLI (short name zudo) creates, runs, builds, extends and checks projects.
  • Applications can be a monolith, a modular monolith or microservices. Start with a monolith.

Next, Your first Zudo code installs your first two ZudoJS packages and uses them in a Task API service.

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.