TJ Holowaychuk’s Express and Koa: Minimal Node Backends
How TJ Holowaychuk’s Express and Koa shaped the Node.js ecosystem: minimalist middleware, composable APIs, and lessons for building maintainable backends.

Why TJ Holowaychuk’s frameworks still matter
TJ Holowaychuk is one of the most influential early builders in the Node.js community. He created Express, helped popularize patterns that shaped how Node web apps are written, and later introduced Koa as a rethink of what a web framework core should be.
Even if you’ve never used his code directly, you’ve almost certainly felt its impact: many Node.js frameworks, tutorials, and production backends inherited ideas that Express and Koa made mainstream.
Minimalist foundations (in plain terms)
Express and Koa are “minimal” in a very specific way: they don’t try to make every decision for you. Instead of shipping a full set of opinions—authentication, database rules, background jobs, admin panels—they focus on a small, reliable core for handling HTTP requests and responses.
Think of it like a well-built toolbox rather than a pre-furnished house. The framework gives you a clear place to plug in features (routing, validation, cookies, sessions), but you decide which pieces you need and how they fit together.
What you’ll learn in this article
This post is a practical tour of what made Express and Koa enduring:
- The key concepts behind their design, especially the middleware pattern.
- The tradeoffs of minimal cores: flexibility and clarity, but also more choices and more responsibility.
- How Express and Koa differ in real projects (structure, control flow, conventions).
- When a minimalist backend is a great fit—and when you might prefer a more “batteries-included” Node framework.
By the end, you should be able to look at a project’s needs (team size, complexity, long-term maintenance) and choose an approach with fewer surprises.
The early Node.js web era and the need for simplicity
Node.js changed what “backend development” felt like for a lot of teams. Instead of switching between JavaScript in the browser and another language on the server, you could build end-to-end in one language, share mental models, and move quickly from idea to working endpoint.
That didn’t just make development faster—it made it more approachable. A frontend-leaning developer could read server code without learning an entirely new ecosystem first, and small teams could ship prototypes and internal tools with fewer handoffs.
What Node.js enabled
Node’s event-driven model and package ecosystem (npm) encouraged fast iteration. You could start with a tiny server, add one dependency at a time, and grow features as real needs appeared.
But early Node also exposed a gap: the built-in HTTP module was powerful, yet very low-level. Handling routing, parsing request bodies, cookies, sessions, and error responses meant rewriting the same plumbing in every project.
The early need: a framework you could understand in an afternoon
Developers didn’t want a heavy “everything included” framework. They wanted a simple way to:
- map URLs to code (routing)
- shape request/response behavior consistently
- add common features without copy-pasting boilerplate
The ideal tool was small enough to learn quickly, but structured enough to stop every app from becoming a one-off tangle of handlers.
Where Express fit
Express arrived at the right moment with a small core and clear conventions. It gave teams a straightforward place to put routes and middleware, without forcing a complex architecture up front.
Just as importantly, Express didn’t try to solve everything. By staying minimal, it left room for the community to build the “optional parts” as add-ons—authentication strategies, validation helpers, logging, templating, and later, API-focused tooling.
That design choice helped Express become a common starting point for countless Node backends, from weekend projects to production services.
Express in one page: what it is and what it does
Express is a lightweight web framework for Node.js. Think of it as a thin layer that helps you accept HTTP requests (like “GET /products”) and send back responses (like JSON, HTML, or a redirect) without forcing you into a big, opinionated structure.
It doesn’t try to define your whole application. Instead, it gives you a few core building blocks—an app object, routing, and middleware—so you can assemble exactly the server you need.
Routing basics: URLs to handlers
At the center of Express is routing: mapping an HTTP method and a URL path to a function.
A handler is just code that runs when a request matches. For example, you can say: when someone requests GET /health, run a function that returns “ok”. When they send POST /login, run a different function that checks credentials and sets a cookie.
This “map routes to functions” approach is easy to reason about because you can read your server like a table of contents: here are the endpoints, here is what each one does.
The request/response lifecycle (no jargon)
When a request arrives, Express hands you two main objects:
- Request: what the client sent (URL, headers, body, cookies)
- Response: what you’re going to send back (status code, headers, body)
Your job is to look at the request, decide what should happen, and finish by sending a response. If you don’t send one, the client waits.
In between, Express can run a chain of helpers (middleware): logging, parsing JSON bodies, checking authentication, handling errors, and more. Each step can do some work and then pass control to the next.
Why Express felt approachable
Express became popular because the surface area is small: a handful of concepts gets you to a working API quickly. The conventions are clear (routes, middleware, req/res), and you can start simple—one file, a few routes—then split things into folders and modules when the project grows.
That “start small, grow as needed” feel is a big part of why Express became a default choice for so many Node backends.
The middleware pattern: the real foundation
Express and Koa are often described as “minimal,” but their real gift is a way of thinking: middleware. Middleware treats a web request as a series of small steps that transform, enrich, or reject it before a response is sent.
Middleware as “small steps”
Instead of one giant request handler that does everything, you build a chain of focused functions. Each one has a single job—add context, validate something, handle an edge case—then passes control along. The app becomes a pipeline: request in, response out.
What middleware usually does
Most production backends rely on a familiar set of steps:
- Logging (recording method, path, timing, status codes)
- Authentication/authorization (who is this user, what can they do)
- Parsing JSON and form bodies (turning raw bytes into usable data)
- Error handling (catching failures and shaping consistent responses)
This is why “minimal” frameworks can still power serious APIs: you add only the behaviors you need, in the order you need them.
Why the model scales in real teams
Middleware scales because it encourages mix-and-match composition. When requirements change—new auth strategy, stricter input validation, different logging—you can swap a step rather than rewrite the app.
It also makes it easier to share patterns across services: “every API has these five middlewares” becomes a team standard.
Just as importantly, middleware shapes code style and folder structure. Teams often organize by layers (e.g., /middleware, /routes, /controllers) or by features (each feature folder contains its route + middleware). Either way, the middleware boundary pushes you toward small, testable units and a consistent flow that new developers can learn quickly.
Koa’s goal: a slimmer core and clearer control flow
Koa is TJ Holowaychuk’s second take on a minimalist Node.js web framework. It was created after Express had proven the “small core + middleware” model could power serious production apps—but also after its early design constraints started to show.
Why Koa was created
Express grew up in a world where callback-heavy APIs were normal and the best ergonomics often came from convenience helpers inside the framework.
Koa’s goal was to step back and make the core even smaller, leaving more decisions to the application. The result is a framework that feels less like a bundled toolkit and more like a clean foundation.
Koa intentionally avoids shipping lots of “standard” features (routing, body parsing, templating). That’s not omission by accident—it’s a nudge toward choosing explicit building blocks for each project.
Cleaner control flow with async/await
One of Koa’s most practical improvements is the way it models request flow. Conceptually, instead of nesting callbacks to “pass control,” Koa encourages middleware that can pause and resume work:
- run code before handing off to the next middleware
awaitthe downstream work- run code after it finishes (ideal for logging, timing, error handling)
This makes it easier to reason about “what happens before and after” a handler, without mental gymnastics.
What stayed the same
Koa keeps the core philosophy that made Express successful:
- middleware composition is still the main abstraction
- the framework stays minimal, focusing on HTTP request/response basics
- the ecosystem fills in the rest
So Koa isn’t “Express but newer.” It’s Express’s minimalist idea pushed further: a slimmer core and a clearer, more structured way to control the request lifecycle.
Express vs Koa: practical differences for real projects
Express and Koa share the same minimalist DNA, but they feel very different once you build something non-trivial. The key difference isn’t “new vs old”—it’s how much structure each framework gives you out of the box.
Learning curve: quick start vs “bring your own parts”
Express is easy to pick up because it has a familiar, straightforward mental model: define routes, attach middleware, send a response. Most tutorials and examples look similar, so new team members become productive quickly.
Koa is simpler at the core, but that also means you assemble more yourself. The async/await-first approach can feel cleaner, yet you’ll make more early decisions (routing, request validation, error handling style) before your app looks “complete.”
Community size and “batteries included” expectations
Express has a larger community, more copy‑pasteable snippets, and more “standard” ways of doing common tasks. Many libraries assume Express conventions.
Koa’s ecosystem is healthy, but it expects you to choose your preferred modules. That’s great when you want control, but it can slow teams that want one obvious stack.
Typical use cases
Express fits:
- Small APIs and prototypes where speed of development matters
- Production services where hiring and onboarding should be easy
- Apps that benefit from the sheer volume of existing middleware and examples
Koa fits:
- Services where you want a slimmer foundation and carefully chosen building blocks
- Teams that care about consistent async control flow and cleaner error propagation
- Internal APIs where you can standardize your own conventions
Decision guide
Choose Express when pragmatism wins: you want the shortest path to a working service, predictable patterns, and minimal debate about tooling.
Choose Koa when you’re willing to “design your framework” a bit: you want a clean core, tighter control over your middleware stack, and fewer legacy conventions influencing your architecture.
Ecosystem effects: why minimal cores create big communities
Express and Koa stay small on purpose: they handle the HTTP request/response cycle, routing basics, and the middleware “pipeline.” By not bundling every feature, they leave space for the community to build the rest.
Small core, big surface area
A minimal framework becomes a stable “attachment point.” Once lots of teams rely on the same simple primitives (request objects, middleware signatures, error handling conventions), it becomes easy to publish add-ons that plug in cleanly.
That’s why Express and Koa sit at the center of huge npm ecosystems—even if the frameworks themselves look tiny.
Common add-on categories include:
- Authentication & sessions (cookies, OAuth, JWT helpers)
- Validation & parsing (schema validation, multipart uploads, body parsers)
- Rate limiting & abuse protection (IP throttling, bot detection, quotas)
- Documentation & tooling (OpenAPI/Swagger generators, request logging, metrics)
The upside: choice and flexibility
This “bring your own building blocks” model lets you tailor a backend to the product. A small internal admin API might need only logging and auth, while a public API might add validation, rate limiting, caching, and observability.
Minimal cores make it easier to adopt only what you need, and to swap components when requirements change.
The downside: you own the integration
The same freedom creates risk:
- Inconsistent quality across packages (maintenance, testing, security practices)
- Breaking changes when a popular middleware updates major versions
- Dependency sprawl, where a simple app pulls in dozens (or hundreds) of transitive dependencies
In practice, Express/Koa ecosystems reward teams that curate a “standard stack,” pin versions, and review dependencies—because the framework won’t do that governance for you.
Reliability and security: what minimal frameworks don’t do for you
Express and Koa are deliberately small: they route requests, help you structure handlers, and enable middleware. That’s a strength—but it also means they won’t automatically give you the “safe defaults” people sometimes assume a web framework includes.
Security basics you must add explicitly
A minimalist backend needs a conscious security checklist. At a minimum:
- Input validation: treat every query param and JSON body as untrusted. Validate types, ranges, required fields, and reject unknown fields where it makes sense.
- Authentication & authorization: the framework won’t decide who a user is or what they can do. You’ll need a clear approach (sessions, tokens, API keys) plus consistent authorization checks.
- Rate limiting: without it, a single client can degrade performance or brute-force endpoints. Add limits per IP/user/token and consider separate limits for expensive routes.
Error handling that supports reliability
Errors are inevitable; what matters is how consistently they’re handled.
In Express, you typically centralize error handling with an error middleware (the one with four arguments). In Koa, you generally wrap the request in a try/catch near the top of the middleware stack.
Good patterns in either:
- Return clear, predictable status codes (400 vs 401 vs 403 vs 404 vs 500).
- Avoid leaking stack traces or internal messages to clients.
- Create an error shape you can rely on (e.g.,
{ code, message, details }) so clients aren’t guessing.
Operational concerns: the “boring” things that keep services alive
Minimal frameworks won’t set up operational essentials for you:
- Structured logging (request ID, user ID, latency, status code) so incidents can be diagnosed.
- Health checks (e.g.,
/health) that verify critical dependencies like databases. - Timeouts for upstream calls (HTTP, DB queries) to prevent hanging requests from consuming all capacity.
Choosing dependencies without importing risk
Most real security issues arrive through packages, not your router.
Prefer well-maintained modules with recent releases, clear ownership, and good documentation. Keep your dependency list small, avoid “one-line helper” packages, and regularly audit for known vulnerabilities.
When you do add middleware, treat it like production code: review defaults, configure it explicitly, and keep it updated.
Keeping a minimalist backend maintainable as it grows
Minimal frameworks like Express and Koa make it easy to start, but they don’t force good boundaries. “Maintainable” isn’t about how few lines you have—it’s about whether the next change is predictable.
What “maintainable” should mean
A maintainable backend is:
- Readable: a new teammate can follow a request from entry point to business rule without guessing.
- Testable: most important behavior can be validated without spinning up the whole app.
- Easy to change: adding a new endpoint or altering a rule doesn’t require touching unrelated files.
If you can’t confidently answer “where would this code live?” the project is already drifting.
Keeping middleware chains understandable
Middleware is powerful, but long chains can turn into “action at a distance,” where a header or error response is set far from the route that triggered it.
A few habits prevent confusion:
- Make middleware purpose-specific (auth, validation, rate limiting), and name it accordingly.
- Avoid hidden branching: prefer returning explicit errors rather than silently skipping logic.
- Keep ordering intentional: document why a middleware must run before another (especially for body parsing, auth, and error handling).
- Use centralized error handling with consistent error shapes so every route doesn’t reinvent responses.
In Koa, be especially careful with await next() placement; in Express, be strict about when you call next(err) versus returning a response.
Structure the project around features, not tech
A simple structure that scales is:
/webfor HTTP concerns (routes, controllers, request parsing)/domainfor business logic (services/use-cases)/datafor persistence (repositories, queries)
Group code by feature (e.g., billing, users) inside those layers. That way, “add a billing rule” doesn’t mean hunting across a “controllers/ services/ utils/ misc” maze.
The key boundary: web code translates HTTP → domain inputs, and the domain returns results that the web layer translates back to HTTP.
A testing mix that matches the boundaries
- Unit tests: focus on domain logic and edge cases (no server, no network).
- Integration tests: test routes end-to-end with an in-memory app instance, validating middleware behavior, status codes, and response bodies.
This split keeps tests fast while still catching real wiring issues—exactly what minimal frameworks leave up to you.
Where Express and Koa sit among modern Node frameworks
Express and Koa still make sense in 2025 because they represent the “small core” end of the Node.js framework spectrum. They don’t try to define your whole application—just the HTTP request/response layer—so they’re often used directly for APIs or as a thin shell around your own modules.
How they compare to newer options
If you want something that feels like Express but is tuned for speed and a bit more modern ergonomics, Fastify is a common step. It keeps the “minimal framework” spirit, but adds a stronger plugin system, schema-friendly validation, and a more opinionated approach to serialization.
If you want a framework that feels more like a full application platform, NestJS sits on the other end: it adds conventions for controllers/services, dependency injection, common modules, and a consistent project structure.
You’ll also see teams reach for “batteries-included” stacks (for example, Next.js API routes for web apps) when the backend is closely tied to a frontend and deployment workflow.
What you gain with more structure
More structured frameworks typically give you:
- Clear conventions (where files go, how features are organized)
- Generators/CLIs to scaffold modules quickly
- Built-in modules (validation patterns, DI, testing helpers, sometimes auth integrations)
This reduces decision fatigue and helps onboard new developers faster.
What you give up
The tradeoff is less flexibility and a larger surface area to learn. You may inherit patterns you don’t need, and upgrades can involve more moving parts.
With Express or Koa, you choose exactly what to add—but you also own those choices.
A practical way to choose
Pick Express/Koa when you need a small API quickly, have a team comfortable making architectural calls, or you’re building a service with unusual requirements.
Pick a more opinionated framework when timelines demand consistency, you expect frequent handoffs, or you want “one standard way” across multiple teams.
Takeaways: building on simple foundations
Express and Koa endure because they bet on a few lasting ideas rather than a long feature list. TJ Holowaychuk’s core contribution wasn’t “another router”—it was a way to keep the server small, predictable, and easy to extend.
The ideas that keep paying off
A minimal core forces clarity. When a framework does less by default, you make fewer accidental choices (templating, ORM style, validation approach) and can adapt to different products—from a tiny webhook receiver to a larger web API.
The middleware pattern is the real superpower. By composing small, single-purpose steps (logging, auth, parsing, rate limiting), you get an application that reads like a pipeline. Express popularized this composition; Koa refined it with a cleaner control flow that makes “what happens next” easier to reason about.
Finally, community extensions are a feature, not a workaround. Minimal frameworks invite ecosystems: routers, auth adapters, request validation, observability, background jobs. The best teams treat these as deliberate building blocks, not random add-ons.
Choosing—and designing—your backend
Pick the framework that matches your team’s preferences and the project’s risk:
- Choose Express when you want maximum familiarity, abundant examples, and a “just works” path for common HTTP services.
- Choose Koa when you value a slimmer core and tighter control over flow and error handling.
Either way, your real architecture decisions live above the framework: how you validate input, structure modules, handle errors, and monitor production.
If you like the minimalist philosophy but want to ship faster, a vibe-coding platform like Koder.ai can be a useful complement. You can describe an API in plain language, generate a working web + backend scaffold, and then apply Express/Koa principles—small middleware layers, clear boundaries, explicit dependencies—without starting from an empty folder. Koder.ai also supports source code export, snapshots/rollback, and deployment/hosting, which can reduce the operational overhead that minimal frameworks intentionally leave to you.
Next reads
If you’re mapping out a Node service, browse more guides in /blog. If you’re evaluating tools or support options for shipping a backend, see /pricing.
A simple checklist for your next Node service
- Define one clear responsibility for the service (and what it won’t do).
- Keep middleware focused: one concern per layer.
- Standardize error handling and response shapes early.
- Validate inputs at the edge (request params/body), not deep inside.
- Add structured logging and basic metrics before launch.
- Track dependencies: remove what you don’t use; pin what you rely on.
- Write a small “how to run and deploy” doc for future you.
FAQ
What does “minimal framework” mean for Express and Koa?
Express and Koa focus on a small HTTP core: routing plus a middleware pipeline. They don’t bundle opinions for auth, database access, background jobs, or project structure, so you add only what your service needs.
That keeps the framework easy to learn and stable over time, but it also means you’re responsible for picking and integrating the rest of the stack.
Why is the middleware pattern the key idea behind both Express and Koa?
Middleware breaks request handling into small, single-purpose steps that run in order (e.g., logging → body parsing → auth → validation → route handler → error handling).
This makes behavior composable: you can swap one step (like auth) without rewriting the whole app, and you can standardize a shared set of middleware across multiple services.
When is Express the better choice in practice?
Pick Express when you want the fastest path to a working service with widely-known conventions.
Common reasons:
- Easier onboarding (many developers already know it)
- Huge ecosystem and lots of “standard” solutions
- Good fit for prototypes, small services, and teams optimizing for familiarity
When does Koa make more sense than Express?
Pick Koa when you want a slimmer core and you’re comfortable assembling the pieces yourself.
It tends to fit well when:
- You want consistent
async/awaitcontrol flow - You prefer explicit choices for routing, parsing, validation, etc.
- You want tighter control over error handling and middleware ordering
How does error handling differ between Express and Koa?
Express middleware typically looks like (req, res, next) and you centralize failures using an error middleware (the one with four arguments).
Koa middleware is usually async (ctx, next) and common practice is a top-level try/catch that wraps await next().
In both cases, aim for predictable status codes and a consistent error body (e.g., { code, message, details }).
How should I structure an Express or Koa project so it stays maintainable?
Start with “edge first, domain inside” boundaries:
/web: routes/controllers, request parsing, response shaping/domain: business rules (services/use-cases)/data: persistence (repositories/queries)
Organize by feature within those layers (e.g., users, billing) so changes stay localized and you can answer “where would this code live?” quickly.
What middleware should I consider “table stakes” for a production API?
A practical baseline for most APIs:
- Request logging + request IDs
- Body parsing (JSON/form/multipart as needed)
- Authentication + authorization checks
- Input validation at the edge (params/query/body)
- Centralized error handling with consistent responses
- Rate limiting (especially for auth and expensive endpoints)
Keep the chain short and purpose-specific; document any ordering constraints.
What security basics do I need to add explicitly with Express or Koa?
Minimal frameworks won’t give you safe defaults, so add them deliberately:
- Validate all external input; reject unexpected fields where appropriate
- Implement auth and authorization (who you are vs. what you can do)
- Add rate limiting and abuse controls
- Avoid leaking stack traces or internal error messages
- Set timeouts for upstream calls (DB/HTTP) to prevent hung requests
Treat middleware configuration as security-critical, not optional.
How do I keep the dependency ecosystem from becoming a liability?
Curate a small “standard stack” and treat third-party packages like production code:
- Prefer well-maintained libraries with recent releases and clear docs
- Pin versions (or use lockfiles) and review major upgrades intentionally
- Avoid “one-line helper” dependencies that add transitive bloat
- Run regular audits (e.g.,
npm audit) and remove unused packages
In minimal ecosystems, most risk comes from dependencies, not the router.
When should I prefer a “batteries-included” Node framework instead?
Choose a more opinionated framework when consistency and scaffolding matter more than flexibility.
Typical signals:
- Many developers will rotate onto the project
- You want a standard architecture across teams/services
- You’d benefit from built-in conventions (modules, DI, validation patterns, testing helpers)
If you’re primarily building HTTP endpoints and want full control over composition, Express/Koa are still a strong fit.