RizTech Academy logo
RizTech Academy
Shipping ItLesson 3 of 535 min

Deploying the API and the database

The API is stateful, it holds connections, and it owns a database that must survive every deployment. That is where the difficulty is, and none of it is solved by picking a good host.

The database first, because it outlives everything

The application can be redeployed a hundred times. The data cannot be redeployed at all.

Managed PostgreSQL, not one you run. Neon, Supabase, Railway, RDS, DigitalOcean — pick one. Running your own means backups, replication, failover, patching and disk monitoring, and on the day it goes wrong you will be doing all of it at two in the morning for a shop that makes a few thousand rupees a day. The managed version costs a few dollars a month and somebody else is on call.

Three things to check before choosing, all of which are cheap now and expensive later:

Automated backups, with point-in-time recovery. The question is not "do you have backups" but "can you restore to 14:32 yesterday, before the bad migration ran?"

Restore, once, before you need it. An untested backup is a belief, not a backup. Restore it into a scratch database and look at the data.

Where it is. A database in Virginia and an API in Mumbai means every query crosses an ocean. A checkout makes several. Put them in the same region, and put that region near your customers.

Connection pooling is not optional on serverless

DATABASE_URL=postgresql://user:pass@host:5432/kirana?connection_limit=10&pool_timeout=20

PostgreSQL handles a few hundred connections. Every serverless function instance opens its own, so a traffic spike opens hundreds at once and the database starts refusing them — including the ones your healthy instances need.

If the API runs as a long-lived container, Prisma's own pool is fine. If it runs on serverless, put PgBouncer or your provider's pooler in front. Getting this wrong looks like "the database is down" under load, when the database is fine and is simply out of connection slots.

Migrations run before the new code

npx prisma migrate deploy

migrate deploy, never migrate dev. dev is interactive, it can reset the database, and it generates new migration files — all correct on your machine and catastrophic in a deployment.

The order matters:

1. run migrations
2. deploy the new code
3. health check passes
4. traffic shifts over

Migrations first, because the new code expects the new schema. Which leads to the rule that makes zero-downtime deployment possible at all:

A migration must be compatible with the code already running.

For a moment — sometimes a long moment — the old code and the new schema are live together. So:

Safe Dangerous
add a nullable column drop a column
add a table rename a column
add an index concurrently add a NOT NULL column with no default
widen a type narrow a type

Renaming deliveryPhone to phone in one migration breaks every running instance the moment it lands. The safe version is four deployments: add the new column, write to both, backfill, then stop reading the old one and drop it.

That feels absurdly cautious until the first time you take a shop down in the middle of a Saturday evening.

Configuration that refuses to be wrong

ConfigModule.forRoot({ isGlobal: true, cache: true, validate: validateEnv })

The first lesson of this module. Worth restating here because deployment is where it pays: a container that will not start is a failed deploy, and the platform keeps the old one running. A container that starts with no JWT secret is an incident.

Verified by actually trying it:

Environment is not usable:
  JWT_SECRET: String must contain at least 32 character(s)
  CORS_ORIGIN: Required
  DATABASE_URL: Invalid input: must start with "postgres"

Two health checks, not one

@Get("live")
live(): { status: "ok" } {
  return { status: "ok" };
}

@Get("ready")
async ready() {
  try {
    await this.prisma.$queryRaw`SELECT 1`;
  } catch {
    throw new ServiceUnavailableException("Not ready");
  }
  return { status: "ok", database: "ok" };
}

They answer different questions, and conflating them causes outages.

Liveness: is the process up? Nothing more. A platform restarts a container that fails this. If liveness checked the database, a database blip would restart every API container at once — turning a recoverable problem into an outage, for no gain, because restarting an API does not fix a database.

Readiness: should this instance get traffic? This one does check, because an API that cannot reach PostgreSQL can serve nothing useful. The load balancer takes it out of rotation and puts it back when it recovers. No restart, no dropped connections.

SELECT 1 rather than a real query: it proves the connection works without depending on any table existing.

And note what the failure does not say:

throw new ServiceUnavailableException("Not ready");

No detail. Health endpoints are unauthenticated, and a connection string in an error body is a connection string on the internet.

Shutting down properly

app.enableShutdownHooks();

When a platform replaces a container it sends SIGTERM and waits — usually about thirty seconds — before killing it.

Without this, the process dies immediately, mid-request. For a checkout that means a customer seeing an error for an order that was actually placed, and then ordering again.

With it, Nest stops accepting new connections, finishes what is in flight, runs onModuleDestroy — which is where PrismaService.$disconnect() lives — and exits. Every deploy becomes invisible instead of a handful of errors.

This is also why the Dockerfile execs node directly rather than going through npm: npm would swallow the signal and none of this would happen.

The image

FROM node:20-slim AS build
…
RUN npm ci
RUN npm run build --workspace=packages/shared \
  && npx prisma generate --schema apps/api/prisma/schema.prisma \
  && npm run build --workspace=apps/api
RUN npm ci --omit=dev && npm cache clean --force

prisma generate at build time, not startup. The generated client is specific to the schema and the platform, so generating it at boot adds seconds to every cold start and can fail in production for the first time.

npm ci --omit=dev after building strips the toolchain the runtime never needs.

RUN apt-get update && apt-get install -y --no-install-recommends openssl \
  && rm -rf /var/lib/apt/lists/*

openssl because Prisma's query engine needs it, and slim images do not have it. The error when it is missing does not mention OpenSSL, which is how it becomes an hour.

Where to run it

Good for Watch out for
Railway / Render one command, a database alongside cost at scale
Fly.io close to your users, cheap you manage more
AWS ECS / Cloud Run you are already there a day of setup
Serverless (Lambda) spiky traffic cold starts, connection limits

For a kirana shop: Railway or Render, with their managed PostgreSQL, in the region nearest the customers. Two hundred rupees a month, and the interesting problems stay in the application.

Serverless is the tempting wrong answer here. A Nest application on Lambda means cold starts on a checkout, and the connection problem above becomes your main operational concern. It is a good fit for spiky, stateless work and a poor one for a stateful API with a database.

CORS, and the thing it is not

app.enableCors({
  origin: process.env.CORS_ORIGIN,
  credentials: true,
});

No fallback, because the config schema already guarantees it is set and a permissive fallback is how an API becomes readable by any site on the internet.

But be clear about what CORS is: a browser-enforced rule, not a security boundary. It stops a page on another origin reading your API with the user's cookies. It stops nothing at all from curl, or a script, or anything that is not a browser.

The actual security is the session cookie, the guards and the signature checks — everything modules 8, 13 and 14 built. CORS is one layer, and the least load- bearing one.

Check your work

Why managed PostgreSQL: backups, failover and patching are somebody else's job, and the alternative is doing all of it at 2am.

Why restore a backup before you need it: an untested backup is a belief.

Why region matters: a checkout makes several queries, and each crosses the ocean twice.

Why pooling matters on serverless: each instance opens its own connections, and PostgreSQL runs out of slots.

Why migrate deploy, never migrate dev: dev is interactive, can reset the database and writes new migration files.

Why migrations must suit the old code: both are live at once during a deploy.

Why liveness must not check the database: a blip would restart every container, and restarting an API does not fix a database.

Why the readiness failure has no detail: the endpoint is unauthenticated.

What enableShutdownHooks buys: in-flight requests finish, so a deploy does not error somebody's checkout.

Why prisma generate at build time: it is specific to schema and platform, and generating at boot can fail in production first.

What CORS is not: a security boundary. It constrains browsers and nothing else.

Practice

  1. Deploy the API to Railway or Render with their PostgreSQL. Run migrate deploy and then the seed.
  2. Hit /api/health/live and /api/health/ready. Then stop the database and hit both again.
  3. Work out what your platform would do to a container failing each one.
  4. Write a migration that renames a column. List the four deployments that would make it safe.
  5. Remove enableShutdownHooks, start a slow request, and redeploy. Watch what the client gets.
  6. Take openssl out of the Dockerfile, rebuild, and read the error. Decide how long it would have taken you.
  7. Move prisma generate to startup and time a cold start against a warm one.
  8. Set CORS_ORIGIN to the wrong domain. Confirm the browser is blocked and curl is not.
  9. Restore a backup into a scratch database and check the data is really there.
  10. Price this shop for a year on two of the platforms above, with the database.

Next: knowing when it breaks, before a customer tells you.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship