Key Takeaways
- Express 5 includes routing and forwards rejected handler promises to error middleware without an async wrapper.
- Koa keeps routing outside its core. Use a separate router such as
@koa/router, not the deprecatedkoa-routerpackage. - Koa’s
await next()wraps downstream middleware. Express’snext()advances a middleware chain; it isn’t an awaitable downstream-completion contract. - Choose for team familiarity, compatible dependencies, and operational requirements. Benchmark your service rather than assuming either framework is faster.
Koa and Express both serve Node.js web applications and APIs. Express offers built-in routing and an ecosystem-first development experience; Koa provides a smaller core and middleware that runs downstream and then unwinds upstream. The useful distinction is how you assemble and control the request pipeline—not whether you can use async/await.
Koa was created by members of the Express team, but it’s a separate framework, not an Express 5 successor or preview. Its deliberately small core remains a design choice, not a promise of better application performance.
Runtime and Example Setup
The examples target Express 5, Koa 3, and @koa/router 15 on Node.js 24. As of October 17, 2026, Node.js 24 is a supported LTS line. Use a supported runtime rather than treating a package’s minimum Node version as a production recommendation.
In a new directory, initialize the project and install the comparison dependencies. The major-version selectors keep these examples on the framework generations discussed here.
npm init -y
npm install express@5 koa@3 @koa/router@15Save the examples as .mjs files: Node treats that extension as an ES module without requiring a package.json type setting. See the Node.js ES module documentation for module-loading rules.
Basic Routing with Express
Save this as express.mjs. Express 5 forwards a rejection from the async route handler to error middleware automatically. The error handler has four arguments and is registered after the routes.
import express from 'express';
const app = express();
async function asyncActivity() {
return { success: true };
}
app.get('/', async (req, res) => {
const result = await asyncActivity();
res.json(result);
});
app.use((error, req, res, next) => {
console.error(error);
if (res.headersSent) {
return next(error);
}
res.status(500).json({ error: 'Internal server error' });
});
app.listen(3000);There’s no asyncWrapper here because this is Express 5. Express 4 applications still need to forward rejected handler promises explicitly; upgrading the package also requires checking the other breaking changes in the migration guide.
Basic Routing with Koa
Save this as koa.mjs. Koa doesn’t include routing; this example adds the separately installed @koa/router package. The older koa-router package is deprecated, and its maintainer notice directs users to the scoped package.
import Koa from 'koa';
import Router from '@koa/router';
const app = new Koa();
const router = new Router();
async function asyncActivity() {
return { success: true };
}
app.on('error', (error) => {
console.error(error);
});
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
if (ctx.headerSent || !ctx.writable) {
throw error;
}
ctx.status = 500;
ctx.body = { error: 'Internal server error' };
ctx.app.emit('error', error, ctx);
}
});
router.get('/', async (ctx) => {
const result = await asyncActivity();
ctx.body = result;
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);The outer middleware catches failures from downstream work that it awaits. The error event listener handles diagnostic logging separately from the client response. router.routes() installs the routes; router.allowedMethods() adds method handling, including OPTIONS responses and appropriate method-error statuses.
Run either server and request the root path on port 3000. Both return {"success":true}. Stop one before starting the other because both examples use the same port.
node express.mjs
# Stop with Ctrl+C before running the other server.
node koa.mjsTo exercise either error path, temporarily throw an error inside asyncActivity(). These examples deliberately return a generic 500 response for unexpected failures. Before adding validation or authentication middleware, extend the error policy to distinguish expected client errors from server failures.
Koa vs Express: Key Differences
| Concern | Express 5 | Koa 3 |
|---|---|---|
| Routing | Built-in route methods and Router API | Separate routing middleware |
| Request and response | Extended Node request and response objects: req and res | Per-request ctx with Koa request and response abstractions |
| Middleware flow | Explicit next() chain | Promise-based downstream and upstream flow |
| Async route failures | Rejected handler promises reach error middleware | An outer try/catch can catch awaited downstream failures |
| HTML rendering | Template-engine integration; engine installed separately | Rendering support assembled separately |
Express’s built-in routing doesn’t make it a batteries-included application platform. In particular, neither framework bundles a template engine: Express provides integration points for an engine you install and configure. Its template-engine guide makes that distinction explicit.
Architecture and Request APIs
Express augments Node’s request and response objects with convenience methods. Koa creates a context for each request: ctx.request and ctx.response are Koa abstractions, while ctx.req and ctx.res still expose the underlying Node objects. Koa wraps those objects; it doesn’t eliminate them.
Express’s res.json(result) sends a JSON response. Assigning an object to Koa’s ctx.body lets Koa serialize it as JSON when it handles the response. That difference matters when middleware wants to inspect or modify a response before it’s sent.
Simplicity, Ecosystem, and Maintenance
For ecosystem breadth and learning resources, I’d start with Express. Its larger npm download footprint and published middleware catalog are useful adoption signals—not guarantees that an individual dependency supports Express 5. Check the actual packages your service needs.
- Parsing: Express includes
express.json(); Koa can add @koa/bodyparser. Parsing isn’t business-rule validation, so plan a separate validation step. - Security: Check maintained integrations for security headers, authentication, and rate limiting. Express’s security guide recommends Helmet, but installing a headers package doesn’t complete your security design.
- Observability: Verify supported framework and router versions in your tracing integration. OpenTelemetry’s Node.js documentation covers instrumentation and points to its framework registry.
These are dependency-selection decisions, not automatic wins for either framework. Koa’s smaller core gives you more assembly choices; it also makes your team responsible for documenting and maintaining those choices.
Koa Middleware vs Express Middleware
Middleware is code that runs around a request handler—for example, authentication, logging, or error translation. Both frameworks support async functions. Their important difference is what happens when middleware hands control onward.
Koa:
A before -> B before -> route
A after <- B after <- return
Express:
A --next()--> B --next()--> async route
Response lifecycle events are observed separately.Koa’s Onion-Style Flow
Calling await next() waits for downstream middleware. Code before it runs on the way in; code after it runs as the stack unwinds. This is the onion model: outer middleware surrounds inner middleware, which makes timing, response transformation, and error boundaries straightforward to compose.
// Fragment: insert before router registration in koa.mjs,
// after the outer error-handling middleware.
app.use(async (ctx, next) => {
console.log('before route');
await next();
console.log(`after route: ${ctx.status}`);
});“After downstream middleware” doesn’t mean the response has finished transmitting, especially for streams. Keep application-pipeline timing separate from HTTP response-completion timing.
Express’s Forward-Moving Chain
Express’s next() moves to the next middleware. It doesn’t return a promise representing downstream completion, so await next() doesn’t recreate Koa’s onion model. For normal response completion, you can observe the response’s finish event instead.
// Fragment: insert before route registration in express.mjs.
app.use((req, res, next) => {
console.log('before route');
res.once('finish', () => {
console.log(`response finished: ${res.statusCode}`);
});
next();
});The finish event doesn’t prove the client received the response; production lifecycle instrumentation should also account for interrupted connections. For the fundamentals of ordering and control transfer, see our Express middleware tutorial.
Async Error Handling Still Has Boundaries
Express 5 observes promises returned by handlers; it doesn’t automatically catch errors from unrelated timers or callback-based APIs. Those paths still need explicit forwarding, such as next(error). In either framework, await or return request-scoped work: a detached promise can escape the intended error boundary.
When to Use Express over Koa
- Your team already understands Express, and the service doesn’t have a concrete requirement that justifies another middleware model.
- Your required authentication, validation, monitoring, or platform integrations have well-maintained Express support.
- You want routing conventions immediately, without choosing and standardizing a separate router.
Express is a reasonable choice for JSON APIs as well as browser-facing applications. Don’t choose it because you think templating is bundled; choose it when its conventions and integrations reduce your team’s work.
When to Use Koa over Express
- You want middleware that naturally wraps downstream execution with before-and-after logic.
- Your team deliberately wants to assemble and own a smaller, explicit HTTP stack.
- You’ve checked the router, body parser, security integrations, and instrumentation against your selected framework versions.
Koa’s minimalism is valuable when that control improves your design. It isn’t a reason to assume your finished application will use materially less memory or have lower latency.
Performance: Benchmark the Service, Not the Framework Name
This comparison doesn’t establish a performance winner. If latency or throughput determines the choice, test representative routes with the same authentication, validation, payloads, database access, logging, runtime, and deployment resources.
Measure tail latency, throughput, errors, CPU, and memory under realistic concurrency. An empty-route benchmark can’t settle whether your actual service meets its requirements. Our Node.js performance optimization guide provides additional areas to investigate.
Conclusion
My decision rule: choose Express when familiarity and integrations remove more work than custom composition would. Choose Koa when its onion middleware model offers a concrete design benefit and you’re comfortable owning the assembled stack. Don’t migrate a healthy application for shorter syntax alone.
Common Mistakes
- Reading old examples as current guidance: don’t add an Express 4 async wrapper to Express 5 by habit, or install deprecated
koa-router. - Dropping the middleware contract: await downstream work in Koa; don’t treat Express’s
next()as a completion promise. - Returning exceptions directly: define a safe response contract instead of sending raw Error objects or internal messages.
- Treating migration as an import change: request APIs and middleware contracts differ. Review routing behavior, adapters, errors, and tests.
Production Checklist
Neither framework replaces operational engineering. Before shipping:
- Security: validate inputs, enforce authorization, configure TLS and cookies appropriately, and trust proxy headers only for your known deployment topology.
- Resource limits: configure request-body limits, timeouts, and rate limits for your service rather than relying on unreviewed defaults.
- Errors and logging: map expected client errors separately from unexpected failures. Use structured logs with request IDs, redact secrets, and keep diagnostic details out of responses.
- Shutdown: stop accepting new traffic, drain active requests within a bounded deadline, then close database connections and other resources.
- Maintenance: commit the dependency lockfile, review runtime and middleware support, and test rejected promises, malformed inputs, missing routes, and interrupted responses.
Use the official graceful-shutdown guidance as a starting point, and review our Express.js security checklist alongside the framework documentation.
FAQ
What is the main difference between Koa and Express?
Express includes routing and uses a next() middleware chain. Koa keeps routing separate and uses promise-based middleware that can run logic before and after awaited downstream execution.
Does Express 5 handle rejected async route handlers?
Yes. Express 5 forwards rejected promises returned by route handlers or middleware to error handling automatically. Errors in detached tasks or unrelated callbacks still need explicit handling.
Does Koa include routing, and what replaced koa-router?
Koa doesn’t include a router. A common separately installed option is @koa/router, which the deprecated koa-router package’s maintainer notice recommends.
Is Koa faster than Express in production?
There’s no workload-independent winner established here. Benchmark representative routes with equivalent middleware, dependencies, and deployment settings; small synthetic benchmark differences shouldn’t determine your framework choice.
Should I choose Koa or Express for a new Node.js API?
Choose Express when team familiarity and compatible integrations simplify delivery. Choose Koa when its onion middleware model provides a concrete benefit and your team is prepared to maintain the assembled stack.
Further Reading
- Koa documentation — core scope, context, and cascading middleware.
- @koa/router documentation — routing APIs, method handling, and compatibility.
- Express error handling — promise failures and custom error middleware.
- Migrating to Express 5 — behavior changes beyond async handlers.
- Using Express middleware — ordering, routers, and built-in parsers.
- Express template-engine integration — installing and configuring rendering support.
- Node.js release status — supported runtime lines.
- Express production security guidance — input handling, headers, cookies, and dependencies.
