Adding a Service
Adding a NestJS service is one command. Name it whatever you want. billing
is used as an example on this guide:
npx nx g @forgestack/tools:service billing # generate new service called "billing"The generator also links the new package into the npm workspace, so there is
no separate npm i to run.
The generator takes two flags:
--dry-run: print everything the generator would create or edit without writing any of it.--expose=false: to generate an internal service that shouldn't be reachable from the browser.
The generated service contains a starter bounded context called example with
one of every building block a bounded context is made of:
- An aggregate:
Greeting, which raises a domain event (GreetingCreated_DomainEvent) when created. - A value object:
GreetingMessage, with validation. - A command:
CreateGreeting_Commandand itsCreateGreeting_CommandHandler, which saves the greeting and writes the publicGreetingCreated_IntegrationEventto the outbox in one transaction, so other services hear about a greeting if and only if it was stored. - A query:
ListGreetings_Queryand itsListGreetings_QueryHandler. - Controllers that dispatch them through
the buses:
CreateGreeting_Controller(POST /api/v1/greetings) andListGreetings_Controller(GET /api/v1/greetings). - A domain-event handler:
GreetingCreated_Log_DomainEventHandler, a small in-process reaction that runs after the command's transaction commits. Put same-service side effects here. - An integration-event handler:
UserCreated_IntegrationEventHandler, consuming another service'suser.createdintegration event from Kafka. The new service greets every registered user out of the box. - Repositories:
Greeting_MongodbRepositoryandGreeting_InMemoryRepository, behind the sameGreeting_Repositoryport interface. - Tests for every handler type, all passing from the first run.
Rename or get rid of the example context once you start building real ones.
Beyond the code, the generator wires the service into the dev and prod compose files, adds a Caddy route for it and creates the env files.
Where the service is served
Every exposed service lives on the same origin as the frontend, under its
own path prefix. Caddy strips the prefix before proxying, so the service itself
still serves /api/v1/.... The prefix follows one rule:
| Service name | Path prefix | Local API URL |
|---|---|---|
service-<N> (e.g. service-3) | /svc<N> | https://localhost/svc3/api/v1 |
any other name (e.g. billing, user-profile) | /svc + the name without dashes | https://localhost/svcbilling/api/v1, https://localhost/svcuserprofile/api/v1 |
The generator prints the exact URLs when it finishes. Because nothing new is added at the DNS level (no subdomain per service), a new service needs no DNS records and no new certificate, and the auth cookies stay first-party, so the browser sends them to every service automatically.
What the generator sets up (and where to edit it)
| Piece | Where | Notes |
|---|---|---|
| Service code | backend/services/<name>/ | Configs, jest, and the example starter bounded context |
| Local config | backend/services/<name>/.env (+ .env.example) | SERVICE_NAME, KAFKA_SERVICE_ID and the Mongo database are unique per service; PROXY_BASE_PATH is the path prefix |
| Dev container | infra/compose/docker-compose.dev.yml | One small block to deploy the service locally |
| Prod container | infra/compose/docker-compose.prod.yml | One small service block to deploy in prod; image tag forgestack-<name>:v1 |
| Prod config | infra/env/<name>.env.prod (+ .example) | Secrets are placeholders (fill them in before deploying) |
| Route | infra/caddy/Caddyfile | A handle_path /svc<...>/* block → <name>:3000 on the main site (skipped with --expose=false) |
That table is the complete list. Everything else (builds, deploys, type-checks, lint, metrics, logs, traces) discovers the new service automatically.
With --expose=false there is no Caddy route: the service is reachable only
inside the Docker network (http://<name>:3000/api/v1) and through the events
it consumes and publishes.
After generating
- Dev: start the stack (
npm run dev:start), or runnpm run dev:restart <name> caddyif it's already running (Caddy re-reads its config only when it restarts). ThenGET https://localhost/svcbilling/api/v1/greetingsconfirms the wiring, and the service's Swagger UI is athttps://localhost/svcbilling/docs. - Prod: fill in
infra/env/<name>.env.prod, using the sameJWT_SECRET,JWT_REFRESH_SECRETandINTERNAL_API_KEYas the other services. No DNS change is needed. Deploy withnpm run prod:updateand choose 5) Update ALL (or 2 then 3), so both the new service image and the Caddy image, which bakes in the Caddyfile, are rebuilt. The API is then served athttps://<your-domain>/svcbilling/api/v1.
To call the new service from the website, see Calling Backend Services.