building-backends
This is the backbone skill for backend work. Read it before writing any server code — it sets the defaults and the non-negotiables so every service comes out consistent, then hands off to the specific skills for the detailed procedures. If a rule here conflicts with a local repo convention, the repo wins; if it conflicts with nothing, follow it.
First, understand before you build
- Read the existing code before adding to it. Match the repo's framework, error model, layering, naming, and test style. A "correct" pattern that fights the codebase is the wrong pattern. Introducing a second way to do something the repo already does is a defect, not an improvement.
- Clarify the contract, not the implementation. Know the inputs, outputs, error cases, who calls it, and the consistency/latency expectations before choosing a design. Most backend rework comes from building the wrong contract fast.
- Prefer boring. Proven, readable, obvious solutions over clever ones. The backend runs unattended at 3am; optimize for the on-call engineer who has to understand it half-asleep, not for elegance.
Architecture — the rules
- Layer with dependencies pointing inward.
transport (http) → services (use-cases) → domain (entities/rules), andservices → repositories (I/O) → domain. The domain imports nothing outward and does no I/O. If an import arrow points the wrong way, the design is broken. - Handlers orchestrate, services decide, repositories do I/O. A handler parses/validates input, calls a service, maps the result to a response — no business logic. A repository is the only place that touches a DB, queue, or external HTTP.
- Keep the domain testable without infrastructure. If you can't unit-test the core logic without a database or network, the logic is in the wrong layer.
- One composition root. Build config, wire dependencies, and start the server in a single place. Everything else receives its dependencies; nothing reaches out to construct global singletons.
- Design stateless. Session/scratch state lives in a shared store (DB/cache), not in process memory, so the service scales horizontally and survives a restart. If you're caching in a module-level variable, stop and think.
→ For standing up the skeleton with these layers already wired, use api-service-scaffold.
API design — the rules
- Explicit, versioned contracts. Version the API (URL or header) from day one; never make a breaking change to a shipped contract — add the new shape alongside and deprecate the old.
- Validate every input at the boundary into a typed value. The core never sees a raw request body. Reject invalid input with 422 and field-level detail; never trust the client.
- One error model, mapped once. Domain code raises typed errors (
NotFoundError,ConflictError,ValidationError,UnauthorizedError…); a single boundary mapper turns them into status +{ code, message, details? }. Never leak stack traces or internal messages.404for "not found" and "not yours" — distinguishing them leaks existence. - Consistent shapes. Same envelope for success, same envelope for errors, consistent pagination (cursor over offset for large sets), consistent field casing. A caller should learn your conventions once.
- Idempotency for unsafe operations.
PUT/DELETEare idempotent by definition; makePOSTs that can be retried safe with an idempotency key. Networks retry — a double-charged customer is a design bug. - Right status codes. 2xx success, 4xx caller's fault, 5xx your fault. A validation failure is 422, not 200-with-an-error-body; an auth failure is 401/403, not 400.
The data layer — the rules
- The database is the source of truth; enforce invariants there. Foreign keys, unique constraints,
NOT NULL, and checks belong in the schema, not only in application code — app code has bugs and races; the DB doesn't lie. - Transactions wrap invariants, and are as short as possible. Everything that must be all-or-nothing goes in one transaction; do no slow work (external calls, heavy compute) while holding one. Long transactions hold locks and starve everyone else.
- Expect concurrency. Two requests will hit the same row at once. Use optimistic locking (a version column) or appropriate isolation; never read-modify-write across a round trip without protection.
- Migrations are backwards-compatible and reversible. Never change a column's meaning in one deploy; expand→migrate→contract. Never take a table-blocking lock on a hot table. Every migration ships with a tested rollback.
- Watch the N+1. Fetch related data in a set, not in a loop of queries. Profile the query count on real endpoints, not just the latency.
- Index the queries you actually run — and only those. An unused index is write-amplification for nothing.
→ For the migration mechanics (locks, expand-contract, backfills, rollbacks), use db-migration-guardian.
Security — the baseline (non-negotiable)
- Authenticate then authorize, on every request, server-side. Never trust a client claim about who it is or what it may do. Check ownership on every resource access — "I have a valid token" ≠ "this record is mine".
- Parameterize every query. No string-built SQL, ever. Same for any injectable interface (shell, LDAP, template).
- Secrets come from the environment/secret manager, never the code. Nothing sensitive in source, logs, or error messages. (See the
secrets-managementskill.) - Least privilege everywhere — DB users, service credentials, cloud roles scoped to exactly what the service needs.
- Rate-limit and set timeouts on inbound endpoints and outbound calls. An unbounded call or an un-limited endpoint is a denial-of-service waiting to happen.
- Validate and encode at the boundaries to prevent injection and SSRF; treat every external input, including from other internal services, as untrusted.
Concurrency, jobs, and failure
- Assume every external call fails. Set timeouts, add bounded retries with backoff + jitter, and a circuit breaker for a flaky dependency. A retry without a timeout is how one slow dependency takes down the whole service.
- Make operations idempotent so a retry (yours or the caller's) is safe.
- Background work goes on a queue, not in the request path. The request enqueues; a worker processes; failures land in a dead-letter queue for inspection, not a silent
catch {}. - Fail loud, degrade gracefully. Surface errors to telemetry; where a non-critical dependency is down, degrade (serve stale, skip the enrichment) rather than 500 the whole request.
Observability is part of "done", not an afterthought
A service you can't debug in production isn't finished. Structured logs, RED/USE metrics, and tracing tied to one correlation id go in as you build, not after the first incident.
→ Use backend-observability for the instrumentation details and the cardinality/PII guardrails.
Testing stance
- Test behavior through the public surface (the endpoint / the use-case), not private internals — so tests survive refactors.
- A pyramid, not an ice-cream cone: many fast unit tests on domain/service logic, a focused set of integration tests over the real DB and wiring, a few end-to-end tests on critical paths.
- Every bug fix gets a regression test that fails before the fix and passes after.
- Deterministic tests. No reliance on wall-clock, network, or ordering; seed data explicitly; isolate tests from each other.
Procedure for any backend task
- Read the surrounding code; nail the contract (inputs/outputs/errors/callers/consistency).
- Place the work in the right layer; keep the domain I/O-free.
- Validate input at the edge; use the one error model; return the right status.
- Put invariants in the DB + a short transaction; handle concurrency; plan migrations as expand-contract.
- Apply the security baseline (authz + ownership, parameterized queries, secrets, timeouts/limits).
- Make external calls and jobs resilient and idempotent.
- Instrument as you go; add tests at the right layer, including a regression test for any bug.
- Confirm against the relevant specific skill's
Definition of done.
Definition of done
- Layers respected (domain does no I/O; dependencies point inward); one composition root.
- Every input validated at the boundary; one error model mapped once; correct status codes; versioned contract.
- Invariants enforced in the DB; transactions short; concurrency handled; migrations backwards-compatible + reversible.
- Security baseline met: authz + ownership checks, parameterized queries, secrets external, timeouts/rate limits present.
- External calls/jobs idempotent and resilient; background work off the request path.
- Observability wired (logs/metrics/traces on a correlation id); tests at the right layers, green, with regression coverage.