# How to Deploy a Koa.js App to Production
Koa was built by the original Express team as a leaner, more modern foundation. It ships almost nothing by default — no router, no body parser — and leans entirely on an elegant async middleware model built around async/await. That minimalism is great for control but means you assemble your production stack deliberately. This tutorial walks through a clean Koa deployment.
Koa's middleware model
Koa's defining feature is its onion model: middleware wraps the request both on the way in and the way out via await next().
import Koa from 'koa';
const app = new Koa();
// Timing middleware: runs before AND after downstream
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // hand off to downstream middleware
ctx.set('X-Response-Time', `${Date.now() - start}ms`);
});Everything — routing, parsing, auth — is middleware you add explicitly.
Step 1: assemble the stack
A realistic Koa app pulls in a router and a body parser:
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
const app = new Koa();
const router = new Router();
router.get('/health', (ctx) => { ctx.body = { ok: true }; });
router.get('/users/:id', async (ctx) => {
ctx.body = await getUser(ctx.params.id);
});
app.use(bodyParser());
app.use(router.routes()).use(router.allowedMethods());Step 2: production-grade error handling
Because Koa is minimal, you own error handling. Put a try/catch middleware at the very top of the chain so it catches everything downstream:
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = { error: ctx.status === 500 ? 'Internal Server Error' : err.message };
ctx.app.emit('error', err, ctx); // for centralized logging
}
});
app.on('error', (err) => console.error('Server error', err));Never leak raw stack traces to clients in production.
Step 3: read config from the environment
const port = Number(process.env.PORT) || 8080;
app.listen(port, () => console.log(`Listening on ${port}`));Step 4: wire a database
Koa does not care about your data layer; use a standard pool driven by an env var:
import pg from 'pg';
export const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
router.get('/todos', async (ctx) => {
const { rows } = await db.query('SELECT * FROM todos ORDER BY id');
ctx.body = rows;
});Run migrations as a release step so the schema is ready before traffic.
Step 5: graceful shutdown
Koa apps should drain in-flight requests when the platform sends SIGTERM (e.g. during a deploy or a spot-node reclaim). Otherwise you drop live requests:
const server = app.listen(port);
process.on('SIGTERM', () => {
server.close(() => {
db.end().then(() => process.exit(0));
});
});Step 6: containerize
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV PORT=8080
EXPOSE 8080
CMD ["node", "server.js"]Koa vs Express, briefly
| Koa | Express | |
|---|---|---|
| Core size | Tiny, no built-ins | Batteries-ish (built-in router) |
| Async model | Native async/await onion | Callback-based (async works but less native) |
| Error handling | You build it | Built-in error middleware |
| Philosophy | Compose what you need | More out of the box |
Neither is "better" — Express is more convenient out of the box; Koa gives a cleaner async core for teams that prefer to assemble their own stack. Both deploy identically as Node containers.
Common pitfalls
- No top-level error middleware. Unhandled errors crash the process or hang requests.
- Forgetting
allowedMethods(). You lose proper 405/501 responses. - Hardcoded port. Always read
process.env.PORT. - No graceful shutdown. Deploys and node reclaims drop in-flight requests.
- Body parser missing.
ctx.request.bodyis undefined and POSTs silently fail.
Deploying on PandaStack
PandaStack auto-detects Node apps or builds from your Dockerfile, so the Koa container above ships from a git push. Provision a managed PostgreSQL and DATABASE_URL is injected automatically into your pg.Pool — no manual wiring. The platform sends SIGTERM for rolling deploys, so the graceful-shutdown handler above works as intended. You get custom domains with automatic SSL, live app logs to watch your error emitter, plus rollbacks and deploy history. The free tier (5 web services + 1 database) is ample for a Koa API with its database.
References
- [Koa.js documentation](https://koajs.com/)
- [@koa/router](https://github.com/koajs/router)
- [koa-bodyparser](https://github.com/koajs/bodyparser)
- [Node.js graceful shutdown patterns](https://nodejs.org/api/process.html#signal-events)
---
Koa rewards teams that like assembling a clean, minimal stack. PandaStack builds your Koa app from one git push and auto-wires the database so you can focus on middleware — start free at [dashboard.pandastack.io](https://dashboard.pandastack.io).