Guide
Designing a small production API
A production starter does not need dozens of abstractions. It needs a clear process boundary, predictable configuration, and enough signals to operate safely.
The first production version should be small enough to understand under pressure.
Make startup deterministic
A service should start from one documented command, bind to the configured port, and fail with a useful error when required configuration is missing. Avoid setup that depends on files or shell state outside the repository and manifest.
Run schema migrations as an explicit release step when the framework supports it. Hiding a destructive migration in process startup makes every replica responsible for coordinating a database change at the same time.
name: orders-api
runtime: node@23
type: api
build_cmd: npm ci && npm run build
release_cmd: npm run migrate
start_cmd: node dist/server.js
health_cmd: node scripts/healthcheck.js
port: 8080Keep the request path visible
Begin with a thin route layer, domain code that can be tested without HTTP, and a small adapter around each external dependency. Add a queue, cache, or second service only when the workload gives you a reason.
Return consistent error shapes and request identifiers. The identifier should travel through logs and outbound calls so one failed request can be followed without searching by timestamp alone.
- —Route layer: parse HTTP input and map domain errors to stable responses.
- —Domain function: enforce order rules without depending on the web framework.
- —Repository adapter: own SQL and transaction boundaries.
- —Provider adapter: apply timeouts and translate external failures.
Ship with operating signals
At minimum, expose readiness, write structured logs, and handle shutdown long enough to finish or reject in-flight requests cleanly. Put timeouts around outbound calls and set resource limits that reveal leaks before they affect neighboring workloads.
These choices are modest, but they create a service the team can debug. Add architecture when the application earns it, not because the starter had room for another folder.
const close = async (signal) => {
app.log.info({ signal }, "shutdown started");
await app.close();
await database.end();
process.exit(0);
};
process.once("SIGTERM", () => close("SIGTERM"));
process.once("SIGINT", () => close("SIGINT"));Exercise one complete failure
Before calling the service production-ready, choose one dependency and fail it deliberately. For the order API, stop PostgreSQL while sending requests. New orders should fail with a bounded 503 response, readiness should remove the replica from rotation, and logs should retain the request ID without printing the connection string.
Restore PostgreSQL and confirm the service becomes ready again without duplicate orders. That single exercise tests more of the real operating contract than a large set of route-only unit tests.