koajs/koa
github.com/koajs/koa ↗ · 108 files · 345 KB
[![gitter][gitter-image]][gitter-url] [![NPM version][npm-image]][npm-url] [![build status][github-action-image]][github-action-url] [![Test coverage][coveralls-image]][coveralls-url] [![OpenCollective Backers][backers-image]](#backers) [![OpenCollective Spons
Overview
Koa 3.2.1 is a Node.js HTTP middleware framework for developers building web applications and APIs. It composes request handlers into a downstream/upstream execution stack and wraps Node request and response objects with context, negotiation, cookie, error, and streaming APIs. The inspected implementation includes optional AsyncLocalStorage and WHATWG response-body support; routing and application persistence are supplied by consumers.
System map
The runtime centers on an EventEmitter-based Application and three linked HTTP wrapper prototypes. Consumers instantiate the library and choose their server and middleware stack. Packaging, CI, and publishing are separate from request execution.
- package.json identifies koa 3.2.1, requires Node >=18, resolves CommonJS to lib/application.js, and resolves ESM to generated dist/koa.mjs.
- Application extends EventEmitter; its constructor owns middleware, settings, and application-specific context/request/response prototypes.
- listen() wraps callback() in http.createServer(); callback() can also be supplied to consumer-created HTTP or HTTPS servers.
- createContext() connects the Node req/res pair to context, request, and response wrappers, records originalUrl, and creates a fresh state object.
- Request handling delegates middleware composition to koa-compose, completion detection to on-finished, and response streaming to Node Stream.pipeline.
- The core state boundary is request-local ctx.state and optional AsyncLocalStorage; the documented database integration is a consumer-supplied app.context.db reference.
- npm scripts provide node --test, c8 coverage, Standard linting, and gen-esm-wrapper building; prepare invokes the build, and published files include dist and lib.
- CI installs, lints, measures coverage, and uploads to Codecov; a separate tag/manual workflow installs and publishes to npm with id-token permission.
Feature catalog
Koa's user-facing surface is a developer API for composing HTTP behavior. Request helpers interpret URLs and headers, while response helpers construct and transmit representations. Cookies, errors, and current-context access complement that core; the documented framework leaves routing to middleware.
- app.use(fn) validates functions and supports chaining; async or promise-returning middleware can await next() and modify the response during upstream unwinding.
- Request APIs expose mutable method, URL, path, query, querystring, and search, alongside original-URL-based href and memoized WHATWG URL.
- accepts(), acceptsEncodings(), acceptsCharsets(), and acceptsLanguages() negotiate headers through accepts; is(), type, charset, and length interpret request content metadata.
- host/hostname, protocol/secure, ips/ip, and subdomains provide proxy-aware request identity; fresh/stale evaluate conditional GET or HEAD requests against outgoing validators.
- Response bodies accept strings, buffers, JSON values, Node streams, ReadableStream, Blob, and Response, with type/status/length handling and HEAD or empty-status suppression.
- Response helpers implement redirect(), back(), attachment(), MIME type assignment, ETag/Last-Modified, Vary, header get/set/append/remove, and flushHeaders().
- ctx.cookies exposes get/set options including signing and security attributes; ctx.throw/ctx.assert and exported createHttpError/HttpError/isHttpError supply HTTP error utilities.
- app.currentContext exposes the current request when AsyncLocalStorage is enabled; raw response handling can be selected with ctx.respond=false, while routing is explicitly external middleware.
Data flow
Incoming Node HTTP objects remain the transport owners throughout a request. Koa adds linked wrappers, lazy parsers, and a middleware-visible state namespace. Outgoing body assignment prepares metadata, and a separate responder handles serialization or streaming.
- callback() receives req/res, constructs a context, and either invokes handleRequest directly or runs it inside ctxStorage.run(ctx).
- Context construction creates wrapper instances from application prototypes but preserves references to the original transport objects; state is allocated independently for each request.
- ctx.query reads req.url through querystring and parseurl, caches by raw query string, and uses URLSearchParams to collapse single values or retain repeated values as arrays.
- Assigning query serializes string/number values and repeated array entries; unsupported value types become empty strings before querystring updates req.url.
- Forwarded IP input is read from app.proxyIpHeader only in proxy mode, optionally right-trimmed by maxIpsCount, then memoized as ip with socket.remoteAddress fallback.
- ctx.body delegates to response.body, which stores _body and selects status, content type, length, cleanup, or Response-derived headers before transmission.
- respond() ends strings/buffers directly, converts WHATWG bodies into Node Readables for pipeline, and JSON-stringifies remaining values while recalculating byte length.
- Cookie data crosses the HTTP boundary through the cookies dependency using req/res, app.keys, and request.secure; Koa's context getter retains only the per-request cookie helper.
Behavior traces
The inspected flows cover startup, a JSON response, signed-cookie output, error handling, and streamed responses. Each follows the implementation from entry boundary through context delegation to the transport. These are source walkthroughs, not executed traces.
- Startup: new Application initializes prototypes/settings → use(fn) appends handlers → listen() calls callback() → callback() composes middleware and installs a default error listener if needed → http.createServer(...).listen(...).
- Request entry: callback's listener → createContext(req,res) → optional ctxStorage.run(ctx) → handleRequest(), which sets 404 and registers onFinished before invoking middleware.
- Middleware cascade: the first handler calls next() → the second runs → the second's continuation runs → the first's continuation runs; the compose test asserts [1,2,3,4] and a final 404 when neither sets a body.
- JSON response: middleware assigns ctx.body → response.body stores the object, defaults status to 200 unless explicit, removes stale length, and chooses JSON type → respond() stringifies and res.end() sends bytes.
- Signed-cookie output: app.keys is configured → middleware accesses ctx.cookies → the getter constructs Cookies(req,res,{keys,secure}) → cookies.set(...,{signed:true}) emits value and .sig cookies; setting 204 ends without a body.
- Error response: ctx.throw() creates and throws an HTTP error → handleRequest's catch invokes ctx.onerror → the application error event fires → unsent headers are cleared/replaced → validated status and exposed-or-generic text are sent.
- Stream response: body assignment recognizes a Node stream and registers destruction on response completion → middleware resolves → respond() selects the stream → Stream.pipeline writes to res and forwards errors to ctx.onerror.
- Return navigation: ctx.back() delegates to response.back() → Referrer/Referer is read → a URL is resolved against ctx.href → matching hosts redirect to the referrer, otherwise alt or '/' → redirect() sets encoded Location and negotiated text/HTML.
Testing signals
Tests and CI were inspected, not executed; test existence is not a passing result. The inspected suites combine Node's test runner, strict assertions, Supertest requests, and synthetic HTTP wrappers. This is a bounded inventory, with explicit limits around mocked integrations and the declared CI matrix.
- The test script uses node --test and coverage uses c8 with lcov/text-summary; application suites import node:test, Supertest, and node:assert/strict rather than relying on the test config's Jest label.
- Application composition tests assert downstream/upstream order and a configurable composer; currentContext tests check concurrent requests, timer/immediate boundaries, disabled mode, and error-event context.
- Response integration tests assert HEAD suppression with preserved JSON/string/buffer lengths, Blob length, ReadableStream chunks, and Response status/header/body forwarding.
- Response-body unit tests exercise custom stream detection, deferred pipeline error ownership, stream-to-stream preservation, stream-to-null destruction, and errors from a replaced stream.
- Cookie and error suites assert missing signing-key errors, .sig output, proxy-derived secure cookies, outgoing-header clearing, err.headers replacement, and errors after headers were flushed.
- Query tests cover repeated keys and unsupported nested-object serialization; back tests cover external and scheme-relative referrers; ESM tests compare default identity and exported property names.
- Integration limits are visible: snapshot tests replace v8.startupSnapshot, HTTP/2 host tests use synthetic request fields, and tests labeled 'from fetch()' locally construct Response objects rather than performing fetch.
- CI declares npm ci, lint, coverage, and Codecov on Node 22/24/26; Node 18/20 are outside this matrix despite the >=18 engine range, and the publish workflow has no explicit lint/test step.
Risk map
These risks describe source-level trust boundaries and coupling, not confirmed vulnerabilities. Proxy identity, cookie security, redirects, error exposure, and stream ownership deserve focused review when modified. Runtime file sources are supplied by consumer middleware in the inspected streaming examples; this investigation did not execute transports, dependencies, or publishing.
- Proxy mode trusts forwarded host/protocol/IP headers; maxIpsCount defaults to unlimited, and the request documentation explicitly describes forged X-Forwarded-For prefixes and configuration mitigations.
- Cookie secure behavior derives from request.secure, which in proxy mode derives from X-Forwarded-Proto; tests deliberately demonstrate this header-to-cookie trust path.
- back() checks URL.host equality, not scheme equality or a full origin; its test accepts an HTTPS referrer for a synthetic request whose default protocol is HTTP.
- Default error handling exposes err.message only through err.expose and clears outgoing headers before applying err.headers; already-sent responses can only emit the error event, not receive a replacement response.
- Stream ownership spans body assignment, on-finished destruction, replacement cleanup, and pipeline. When cleanupPreviousStream is reached, previous Node streams survive stream-to-stream replacement and are destroyed for non-stream replacements; the null-body branch with application/json type returns early before that cleanup.
- Importing application.js changes createHttpError.HttpError[Symbol.hasInstance] on the dependency constructor; the predicate requires an Error with expose and matching numeric status/statusCode.
- URL views intentionally differ: mutable path/query update req.url, while href and memoized URL use originalUrl; URL is not invalidated by the URL setter.
- ctx.respond=false bypasses the responder entirely, and the context documentation calls this unsupported because it can break Koa and middleware behavior.
Extension points
Middleware registration is the principal extension mechanism, supported by configurable composition and application-specific prototypes. Transport integration and error instrumentation are explicit API boundaries. Consumer services and parsers plug into those boundaries rather than a central route or provider registry.
- app.use(fn) is the ordered extension registry: it validates a function, appends it to middleware, and returns the application for chaining.
- options.compose replaces koa-compose and receives the middleware array when callback() is created; handleRequest expects the resulting middleware invocation to return a promise.
- app.context supports application-wide helpers such as a database reference, while createContext() supplies per-request objects inheriting that prototype.
- app.request and app.response are application-specific prototypes; new shortcut APIs must also account for the explicit delegates table on the context prototype.
- callback() is the transport adapter for consumer-created HTTP/HTTPS servers and documented Connect/Express mounting.
- Custom error behavior can use upstream try/catch middleware or app.on('error'); a caught error that is not rethrown does not reach the default error event path.
- Consumers can supply their own AsyncLocalStorage instance or replace ctx.cookies; the source and tests expose both customization seams.
- The middleware guide recommends option-taking factories and named middleware; independent middleware groups can be combined with koa-compose, and handlers can terminate downstream dispatch by omitting next().
Contributor handoff
Begin with the lifecycle and paired response implementations, then inspect the API-specific tests. Tests and CI were inspected, not executed, so future validation must establish its own results. Preserve the library's documented extension boundaries and treat this report as a bounded source inventory.
- Read package.json and application.js first to understand module exports, generated ESM output, constructor options, middleware composition, and request completion.
- For body changes, read response.body and application.respond together; metadata assignment and transport output are separate and share explicit-status/null and stream assumptions.
- Preserve the context delegation contract when adding request or response properties; the FAQ requires generally useful APIs and consistent getter/setter ownership.
- Use request-local ctx.state for middleware data; putting mutable data on app.context shares it through the application prototype.
- The established checks are npm test, npm run lint, and npm run test:coverage; inspected test files use node:test and can support focused file selection when execution is authorized.
- ESM loading depends on generated dist/koa.mjs; npm prepare builds it, and the ESM suite checks parity with the CommonJS constructor and exports.
- Follow two-space formatting and Standard linting; distinguish legacy comments/config from current behavior, since use() mentions conversion but only registers functions, v3 docs remove old middleware support, and the test config still labels Jest.
- Do not infer runtime verification from inspected fixtures: HTTP/2 host tests synthesize fields, snapshot tests simulate callbacks, fetch-named tests construct local Responses, and declared CI steps provide no execution result in this brief.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 82 | 100% |
| Markdown | 14 | docs/config |
| YAML | 6 | docs/config |
| JSON | 1 | docs/config |
Start reading here
Probable source entrypoint: lib/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (7 files)
Source layout — lib/
| Path | Files |
|---|---|
lib/application.js | 1 |
lib/context.js | 1 |
lib/is-stream.js | 1 |
lib/only.js | 1 |
lib/request.js | 1 |
lib/response.js | 1 |
lib/search-params.js | 1 |
Repository structure
| Top-level path | Files |
|---|---|
__tests__/ | 74 |
docs/ | 11 |
lib/ | 7 |
.github/ | 4 |
test-helpers/ | 2 |
Docs found: Readme.mddocs/api/context.mddocs/api/index.mddocs/api/request.mddocs/api/response.mddocs/error-handling.mddocs/faq.mddocs/guide.mddocs/koa-vs-express.mddocs/migration-v1-to-v2.md
Package
package.json — koa v3.2.1, 23 declared dependencies.
acceptsc8content-dispositioncontent-typecookiesdelegatesdestroyencodeurlescape-htmlfreshgen-esm-wrapperhttp-assert
CI & config entrypoints
.github/workflows/node.js.yml— GitHub Actions workflow.github/workflows/npm-publish.yml— GitHub Actions workflowpackage.json— npm scripts: authors, build, lint, lint:fix, lint:pretty, prepare, test, test:coverage
Structural health signals
Average score 9.1/10 across 82 analyzed source files; 2 hotspots surfaced.
__tests__/application/respond.test.js— 1172 lines, score 5.2 (Large file, Deep nesting)__tests__/context/onerror.test.js— 258 lines, score 6.4 (Deep nesting, Long function/block)lib/request.js— 750 lines, score 6.7 (Large file, Long function/block)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
824c1cf8de9a2026-09-19 — fix(response): keep the Blob content type instead of forcing octet-stream (#1987)c3c14b6979d32026-09-13 — docs: point request.hostname URL link at current Node.js docs (#1994)571938d1b42d2026-09-03 — fix: parse absolute-form request targets in request.URL (#1999)10617764ac2b2026-09-02 — patch: make ctx.assert errors instanceof HttpError (#1998)4a191b1fb7bc2026-08-23 — ci: bump node versions to 22-26 (#1972)