Org Skills

nodejs-architecture

Design and review the architecture of Node.js/TypeScript back-end systems — modular monolith and feature modules, ports-and-adapters, NestJS modules vs Fastify plugins, dependency injection, domain vs transport separation, monorepos and shared packages, event-driven messaging with outbox and idempotency, API style choice, versioning, multi-tenancy, OpenTelemetry and deployment trade-offs.

Download .zip Raw Source
When agents use itUse this whenever the user is structuring a new Node service, asks where code should live, splits or merges services, sets up pnpm workspaces, Nx or Turborepo, edits app.module.ts, nest-cli.json, turbo.json, nx.json, pnpm-workspace.yaml or package.json workspaces, compares REST, GraphQL, tRPC or gRPC, adds Kafka, RabbitMQ, SQS or BullMQ, or asks "microservices or monolith?" or "containers or serverless?". Also apply it when reviewing a Node.js codebase's structure or an architecture proposal or ADR.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill nodejs-architecture -a github-copilot
# or with the org installer (adds .github/skills/nodejs-architecture):
npx -y github:AGCO-Global/org-skills add skill nodejs-architecture

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill nodejs-architecture
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill nodejs-architecture -g

Installs the backend-nodejs-skills plugin, which bundles all Backend / Node.js skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install backend-nodejs-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill nodejs-architecture -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill nodejs-architecture -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill nodejs-architecture -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill nodejs-architecture -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

Node.js Architecture

Start as a well-bounded modular monolith and earn every network hop. Most Node back-ends fail from tangled modules and hidden coupling, not from lack of microservices; the architecture's job is to keep the domain testable without HTTP, the database or the broker.

1. Decide the shape first

Situation Default Change when
One team, one product, evolving domain Modular monolith: one deployable, feature modules with enforced boundaries A module needs independent scaling, release cadence or a different runtime
Several teams, stable bounded contexts Services per bounded context, async integration Teams share one DB schema: fix ownership before splitting
Spiky, event-driven, low steady traffic Serverless functions around a shared domain package Sustained load, long connections (WebSockets), heavy cold-start cost
CPU-heavy pipelines Separate worker service (queue-fed) Never put them in the API process

Rule: a service owns its data. Two services writing the same tables is a distributed monolith.

2. Inside a service: feature modules + ports and adapters (lite)

src/
  modules/orders/
    domain/        Order entity, value objects, domain errors, pure rules (no I/O imports)
    application/   use cases: PlaceOrder, CancelOrder; depend on ports (interfaces)
    ports.ts       OrderRepository, PaymentGateway, EventPublisher interfaces
    adapters/      http routes, Prisma/Drizzle repository, payment client, message consumer
    index.ts       the module's public API; other modules import only this
  platform/        config, logger, db, telemetry, error mapping (shared infrastructure)
  app.ts           composition root: wires adapters to use cases, registers routes
  • Transport (HTTP, queue consumer, cron, CLI) is a thin adapter that parses input, calls a use case, maps the result. The same use case serves REST and a Kafka consumer.
  • Domain code never imports fastify, express, @nestjs/*, ORM clients or process.env.
  • Enforce boundaries with eslint-plugin-boundaries, dependency-cruiser or Nx module boundary rules; CI fails on cross-module deep imports.
  • Keep it "lite": ports only where a second implementation exists or tests need it (DB, external APIs, clock, id generation). Don't wrap every function in an interface.

3. Framework structure and DI

Option Fits Notes
NestJS modules Large codebases, teams used to Angular/Spring patterns One Nest module per feature; export only providers others need; forwardRef is a smell signalling a cycle to break with events or a shared module
Fastify plugins Lean services One encapsulated plugin per feature; decorate shared infra (db, config) via fastify-plugin; features don't reach into each other's decorators
Manual composition root Any framework; smallest magic Plain constructor/factory injection in app.ts; easiest to read and test
awilix / tsyringe / inversify Many dependencies without Nest Prefer awilix (no decorators) or keep the container at the edge; never call the container from domain code

Pass dependencies explicitly (constructor or factory args). Module-level singletons imported everywhere (import { db } from '../db') make testing and multi-tenant wiring painful.

4. API style

Style Choose when Avoid when
REST + OpenAPI Public/partner APIs, cacheable resources, broad client mix Clients need highly variable nested shapes
GraphQL (Apollo, Mercurius, Yoga) Many client types aggregating many entities, BFF layer Simple CRUD; you can't invest in DataLoader, depth/complexity limits, persisted queries
tRPC TS front end and back end in the same monorepo, one team Public or non-TS consumers
gRPC / Connect Service-to-service, streaming, strict contracts across languages Browser clients without a proxy

Versioning: additive changes by default; breaking changes via URL (/v2) or media-type version for REST, field deprecation for GraphQL, new package/service version for gRPC. Publish a deprecation window and measure old-version traffic before removal.

5. Messaging and event-driven design

  • Queue (BullMQ, SQS, RabbitMQ) for work distribution/commands; log (Kafka, Redpanda, Kinesis) for event streams with replay and multiple consumer groups.
  • Transactional outbox: write the domain change and an outbox row in one DB transaction; a relay publishes and marks sent. Never "save then publish" as two independent steps: one will eventually fail alone.
  • Idempotent consumers: delivery is at-least-once. Dedupe by message id in a processed-messages table (same transaction as the effect) or make the effect naturally idempotent (upserts, conditional updates).
  • Events are facts in past tense (OrderPlaced) with a versioned schema (JSON Schema, Avro, Protobuf + registry); consumers tolerate unknown fields.
  • Ordering only per key (partition by aggregate id); design for retries, poison messages and a dead-letter queue with alerting.
  • Sagas/process managers for multi-service workflows; consider a workflow engine (Temporal) when compensation logic grows.

6. Multi-tenancy

Model Isolation Cost
Shared tables + tenant_id Lowest; enforce with row-level security or a mandatory repository filter Cheapest, simplest ops
Schema per tenant Medium Migration fan-out, connection handling
Database per tenant Highest, per-tenant backup/residency Most ops overhead

Resolve tenant once at the edge (token claim, subdomain), carry it in AsyncLocalStorage request context, and make it impossible to query without it. Per-tenant rate limits and noisy-neighbour protection belong in the design.

7. Operability: 12-factor, observability, runtime

  • 12-factor: config from env (validated once), stateless processes, logs to stdout, backing services as attached resources, dev/prod parity, admin tasks as one-off processes.
  • OpenTelemetry from day one: @opentelemetry/sdk-node with auto-instrumentations loaded before the app (node --import ./instrumentation.js); propagate W3C trace context across HTTP and message headers; correlate pino logs with trace_id. RED metrics per route, queue depth and consumer lag per topic.
  • Health: separate liveness (process alive) and readiness (dependencies reachable, not shutting down).
  • Containers vs serverless: containers for steady traffic, long-lived connections, connection pools and background consumers; serverless for bursty, event-triggered work, accepting cold starts, per-invocation limits and the need for DB poolers. Keep the domain runtime-agnostic so the choice stays reversible.

8. Monorepos and shared packages

  • pnpm workspaces as the base; Turborepo for task caching with minimal opinion; Nx when you want generators, affected-graph CI and enforced module boundaries.
  • Layout: apps/<service>, packages/<domain-or-lib>, packages/tsconfig, packages/eslint-config; internal deps via workspace:*; TypeScript project references for incremental builds.
  • Share contracts (schemas, generated clients, event types) and platform utilities. Do not share domain entities between services; that recouples them.
  • Build and deploy only affected apps; each app gets its own Docker image built from a pruned workspace (turbo prune, pnpm deploy).

Deliverable format

Produce an ADR (Architecture Decision Record) plus a structure sketch:

# ADR-NNN: <decision>
Status: Proposed | Accepted     Date: <yyyy-mm-dd>
## Context       forces, constraints, team size, load, SLAs
## Options       2–4 options with the decision-table trade-offs
## Decision      the choice and the one condition that would flip it
## Consequences  what gets easier, what gets harder, migration steps
## Structure     folder tree / module map / sequence of messages

Anti-patterns to reject

  • Microservices from day one for one team; services sharing a database.
  • Domain logic in route handlers, NestJS controllers or ORM entities with framework decorators leaking inward.
  • Cross-module deep imports; utils/ dumping ground; circular modules patched with forwardRef.
  • Publishing events outside the DB transaction without an outbox; non-idempotent consumers.
  • Shared "common" package containing entities used by every service.
  • GraphQL without DataLoader and query cost limits; tRPC exposed to third parties.
  • Tenant filtering left to each query author's memory.
  • Observability added after the first incident; one health endpoint for both liveness and readiness.