2. Concepts — modules & the framework
This chapter is the idea behind Damat: what a module is, why the backend is shaped this way, and how the framework turns a folder of modules into a running server. Read it once and the rest of the guide will click into place.
The problem Damat solves
Most backend frameworks make you choose between two bad options:
- a monolith where every feature is entangled with every other, so adding billing means touching auth, and reusing a feature in another app means copy-pasting and re-wiring; or
- a pile of libraries you assemble yourself, where there is no standard shape for a feature, no shared lifecycle, and no way to share a whole feature (its tables, migrations, service, and config) as one thing.
Damat's answer is the module: a standard, self-contained shape for a feature, plus a framework that knows how to wire any number of modules together.
What a module is
A module is a vertical slice of a backend — everything one domain concern needs, in one folder:
modules/user/
├── module.json # the portable contract (name, env, packages, deps, registry)
├── index.ts # defineModule(...) — the module's public definition
├── service.ts # ModuleService({ models, credentialsSchema }) — data + logic
├── config/ # a zod schema + a loader for the module's credentials
├── models/ # ORM model definitions (its tables)
├── migrations/ # SQL migrations (its schema history)
└── workflows/ # optional saga workflows
Three properties make this powerful:
- Self-contained. A module owns its tables, its migrations, its service, and the env/credentials it needs. Nothing about a module is scattered across the app.
- Portable. Because the shape is standard and described by
module.json, the same module can be dropped into any Damat app —damat module addcopies it in, registers it, syncs its env vars, and installs its npm deps. See MODULES.md. - Independently developable. A module can run and be tested on its own,
with no surrounding app, using the
@damatjs/moduleharness. You build and verify a feature in isolation, then ship it.
A useful analogy: modules are to a Damat backend what components are to a frontend — a standard unit you compose, reuse, and share.
How the backend is a framework
You write modules and route files; the framework does the wiring. When your app
starts (@damatjs/framework drives this),
it:
- loads & validates config — reads
damat.config.ts, including each module's credentials (validated against its zod schema, so bad config fails fast at boot, not at request time); - connects infrastructure — opens the PostgreSQL pool
(
orm-connector+orm-pg) and, ifredisUrlis set, Redis (@damatjs/redis); - initializes modules — instantiates each module's service against the
shared pool and registers it so it can be retrieved anywhere with
getModule(id); - builds the HTTP layer — scans
src/api/routes/**/route.tsinto a Hono router, applies middleware (CORS, headers, request IDs, error handling), and exposes health/introspection endpoints; - starts & guards the server — listens via
@hono/node-serverand installs graceful SIGINT/SIGTERM shutdown.
You never write that bootstrap. You declare what (modules + routes); the framework decides how and when.
The four layers you compose
┌────────────────────────────────────────────┐
Routes │ src/api/routes/**/route.ts (HTTP surface) │
└───────────────┬────────────────────────────┘
│ getModule(id)
┌───────────────▼────────────────────────────┐
Workflows │ steps + sagas (multi-step, compensating) │
└───────────────┬────────────────────────────┘
│ calls
┌───────────────▼────────────────────────────┐
Services │ ModuleService: CRUD + transactions + logic │
└───────────────┬────────────────────────────┘
│ uses
┌───────────────▼────────────────────────────┐
Models │ the ORM DSL → tables, relations, migrations│
└─────────────────────────────────────────────┘
- Models define your tables with the ORM DSL.
- Services turn models into typed CRUD plus your business logic (ModuleService).
- Modules bundle models + service + config + migrations into a portable
unit registered in
damat.config.ts. - Routes and workflows expose and orchestrate that logic (HTTP, workflows).
A typical request flows down and back:
route → module service (CRUD / transaction) → ORM → Postgres.
A multi-step operation that must roll back on failure goes through a workflow:
route → workflow → steps → module services, with the engine running
compensations in reverse if a step throws.
How modules compose
Modules stay decoupled, but real apps need them to work together. Damat offers three mechanisms:
getModule(id)— at runtime, any route, step, or service can fetch another module's service by id. This is the everyday way modules call each other.- Links (
src/links/) — declare cross-module relationships outside the modules themselves, so neither module hard-depends on the other's tables. A link generates a junction table and agetModule("link")service to create, dismiss, fetch, and graph-query across modules. See@damatjs/linkfor the full model. - Pairing hints — a module can leave a non-binding
pairsWithhint in itsmodule.jsonsuggesting modules it works well with. It's a comment for the backend owner, who decides what to actually install and link — a module never dictates composition. (A hardmodulesdependency exists as a rare escape hatch; installing warns if one is missing.)
The module lifecycle
author ──► validate ──► (publish) ──► install ──► migrate ──► run
│ │ │ │
│ │ │ └─ damat-orm migrate:up
│ │ └─ damat module add <source>
│ └─ damat module validate (no warnings = registry-ready)
└─ damat module init / dev / migration:create / codegen
- Author & test a module standalone (chapter 13).
- Validate it against the contract; clean it up until it's registry-ready.
- Publish it to a registry (or just push it to git / keep it local).
- Install it into an app — from a registry ref, a path, or git (chapter 14).
- Migrate to apply its schema, then run.
Trust & the registry
Modules are addressed by ref (user, damatjs/user@0.2.0). A registry maps
a ref to a fetchable source plus trust metadata — a verifiable owner and a
verification status the registry backend stamps (an author cannot
self-verify). At install time a policy (DAMAT_MODULE_VERIFY: off / warn /
require) decides whether unverified modules may be installed; rejected and
revoked modules are always blocked. This is what makes "install a module by
name" safe. Full model: MODULES.md and
the AI install chapter.
When to reach for what
| You want to… | Use |
|---|---|
| Add a table | a model in a module's models/ |
| Add reusable data logic | a method on the module's service |
| Expose an endpoint | a route file |
| Coordinate steps that must roll back | a workflow |
| Cache / rate-limit / queue / lock | @damatjs/redis |
| Package a feature for reuse | a module + module.json |
| Pull in someone else's feature | install a module |