Docs / Recommended libraries & conventions for a Node app
Guidedeploymill://guides/conventions/node

Recommended libraries & conventions — Node (deploymill)

You are an agent about to write or extend application code in a deploymill-managed Node app. This guide is deploymill's paved road: the libraries and patterns that are known to work with the platform's constraints, plus where deploymill has no opinion and you should defer to the user or the repo.

Read it together with deploymill://guides/stack/node, which documents the platform contract — the things that make the deploy succeed or fail. This guide is about choices; the stack guide is about requirements.

How to read these recommendations

Recommendations come in three strengths. Treat them differently:

  • Contract (must). Lives in deploymill://guides/stack/node, not here. Violate it and the deploy breaks (bind to 0.0.0.0, listen on $PORT, ephemeral filesystem, slim runtime stage).
  • Paved road (should). The defaults below. They're not arbitrary — every one is either already in the template deploymill scaffolds for you or already used by the database playbook (deploymill://guides/database/node). Following them keeps the app consistent with what the rest of the tooling assumes. You can deviate; each row says when that's reasonable and what you have to preserve if you do.
  • Taste (deploymill has no opinion). Formatting, test runner, folder layout beyond the template. deploymill is an orchestration layer, not a framework — it won't dictate these. Defer to the user, or to the repo's .deploymill/conventions.md if one exists (see the bottom of this guide).

The rule of thumb: the closer a choice sits to a platform constraint, the more weight the recommendation carries. A pooled DB client is a strong recommendation because Neon caps connections; a formatter is no recommendation at all.

Paved road

These are the choices the node template and the database guide already commit to. Stay on them unless you have a concrete reason.

ConcernDefaultWhy (tie to the platform)When to deviate
RuntimeNode 22 (node:22-alpine)The scaffolded Dockerfile's base image.Only with a matching base-image bump in the Dockerfile.
Module systemESM ("type": "module")The template sets it; migrations and all .js are loaded as ESM (CommonJS exports.up throws — see the database guide).Rarely; if you switch, fix every .js including migrations.
Package managerpnpm via corepack (pinned packageManager)The Dockerfile uses corepack; the pin keeps pnpm on a version compatible with the Node base. Don't strip the field.
Web frameworkHono + @hono/node-serverWhat the template scaffolds; its default binds to all interfaces, satisfying the 0.0.0.0 contract for free.Existing Express/Fastify app — keep the PORT contract and /healthz.
Postgres clientpg (^8), one pooled module at module scopeThe database guide's pool respects Neon's connection limit; a per-request pool exhausts it.
Migrationsnode-pg-migrate (^7), programmatic runner with noLock, run on container startNeon's pooled endpoint is pgbouncer txn-mode — the default session lock hangs. The guide's runner handles this.
Health routes/healthz (liveness), /db (DB reachability)deploymill's liveness convention and the deploy probePath target.Keep a stable replacement path if you move them.

What the paved road steers you away from (from the database guide's "What NOT to do"): don't reach for a heavyweight ORM (Prisma, TypeORM) unless the user explicitly asks. They add build steps, generated clients, and coupling that work against the "scaffolded and simple" posture. Plain pg + node-pg-migrate covers most scaffolded apps.

When the user needs a capability the template doesn't ship

These aren't in the scaffold, so there's no forced default. The picks below are recommended-but-optional — reach for them when the user asks for the capability, and feel free to substitute if they have a preference. Each one is tied to something deploymill already gives you.

  • Auth → Better Auth. It's a SQL-backed auth library, so it slots onto the managed Postgres you already provision via database: { provider: "neon" } — no extra infra. (It's also what deploymill itself runs on.) If the user wants a hosted IdP (Clerk, Auth0) instead, that's fine; just remember host-pinned callback URLs must be set per-environment via set_env_vars (and per-preview via create_preview envOverrides — see deploymill://guides/previews). Read deploymill://guides/auth before wiring auth — it covers the platform facts that trip up login on deploy/preview (sessions in Postgres, host-pinned base URL, secrets via the vault, HTTPS-vs-secure-cookies).
  • Input validation → zod. Widely used, zero runtime infra, pairs naturally with Hono handlers. Optional.
  • TypeScript. Supported — the stack guide's "Switch to TypeScript" section has the Dockerfile changes. Add @types/pg if you've wired up the database.

Things deploymill has no opinion on

Don't impose a default here. If the user hasn't said and there's no .deploymill/conventions.md, pick the least surprising option and move on — these have no platform consequence:

  • Code formatting / linting (Prettier, ESLint, Biome).
  • Test framework (node:test, Vitest, Jest).
  • Folder structure beyond what the template scaffolds.
  • Logging library, naming conventions.

Project-level overrides: .deploymill/conventions.md

A repo (or the user) may pin its own house style. Before applying the defaults above, check for a .deploymill/conventions.md file in the repo (read it with get_file). If present, it overrides this guide wherever the two disagree — it's the team's explicit choice and wins over deploymill's generic paved road. The platform contract in the stack guide still holds regardless; a project can't opt out of binding to $PORT.

This is the same file-as-truth pattern as .deploymill/project.json: conventions live in the repo, version with the code, and travel with the project.