Overview
Adapter library for Koa, a popular HTTP microframework for Node.js. Allows you to write Koa handlers as ƒ(request) -> response
, similar to Ring in Clojure. See motivation.
Includes optional support for implicit cancelation via Posterus futures and coroutines/fibers. See motivation.
TOC
Usage
Shell:
npm install --exact koa-ring
Node:
const Koa = const toKoaMiddleware = const app = app { return { // do whatever // can substitute request const req = const response = await // can substitute response return response || status: 404 }} { // Status and headers are optional return status: 200 headers: {} body: 'Hello world!'} { return Object} const PORT = 9756 app
With cancelation support:
const Koa = const toKoaMiddleware = const Future = const app = app // Implicitly converted to a Posterus fiber by koa-ring { // Could be a future-based database request, etc // This work can be automatically canceled if client disconnects // koa-ring automatically calls future.deinit() const greeting = Future return body: greeting} const PORT = 9756 app
See API below.
Motivation
Functional Programming
In Koa, request handlers take a request/response context object, return void
and mutate the context to send the response. In other words, Koa is a poor match for the HTTP programming model, which lends itself to plain functions of ƒ(request) -> response
.
Advantages of ƒ(request) -> response
:
-
Easy to rewrite request and/or response at handler level
-
Lends itself to function composition
-
You can often return response from another source, without writing a single line of context-mutating code
-
Returning nothing instead of a response makes it easy to signal "not found" or "noop" to the calling handler
Fortunately, we can fix this. We have the technology to write functions.
Cancelation
JS promises lack cancelation, and are therefore fundamentally broken. Particularly unfit for user-facing programs such as servers. On a server, you want each incoming request to own the work it starts. When the request prematurely ends, this work must be aborted.
-
In Erlang, this tends to be the default: you create subprocesses using
spawn_link
, they're owned by the handler process and die with it. -
In thread-based languages, this also works as long as you don't spawn another thread, as there's no analog of
spawn_link
. -
In Go, you're out of luck, as there's no support for goroutine cancelation.
-
In Node.js, you can achieve this by using cancelable async primitives, such as Posterus futures, and coroutines built on them.
Concrete example:
const Future = // const {fiber} = require('posterus/fiber') { // If client disconnects, this invokes onDeinit, aborting work const one = // Delegate to another fiber, implicitly owning it; // if the client disconnects, both routines are canceled, aborting work const other = return body: other} // Futures can be canceled with `future.deinit()` { const future = const operationId = return future} // Routines are started as `const future = fiber(generatorFunction(...args))`// and canceled as `future.deinit()` { const value = return value}
Lack of implicit cancelation leads to incorrect behavior. The client may wish to abort the work it has started; smart clients may cancel unnecessary requests to avoid wasting resources; and so on. Worse, this makes Node.js and Go servers uniquely vulnerable to a certain type of DoS attack: making the server start expensive work and immediately canceling the request to free the attacker's system resources, while the server keeps slogging.
Fortunately, we can fix this. We have tools for implicit ownerhip and cancelation in async operations, such as Posterus.
API
In Koa, every request handler acts as middleware: it controls the execution of the next handler, running code before and after it.
In koa-ring
, these are separate concepts. A middleware function creates a request handler function by wrapping the next handler.
// Response shape. Status and headers are optionalconst mockResponse = status: 200 headers: {} body: 'Hello world!' const handler = mockResponse const overwritingMiddleware = async { const ignoredResponse = await return mockResponse} const noopMiddleware = nextHandler const endware = handler
The resulting handlers have a signature of ƒ(request) -> response
and lend themselves to composition and functional transformations of requests/responses.
koa-ring
doesn't provide any special tools for middleware. Wrap your handlers into middlewares before passing the final handler to toKoaMiddleware
and then to koa.use
.
Request
Every handler receives a request, which is a plain JS dict with the following shape:
interface Request url: string location: Location method: string headers: {} body: any ctx: KoaContext // Unimportant fields omitted interface Location pathname: string search: string query: {} // Unimportant fields omitted
request.ctx
is the Koa context. It provides access to additional information and the underlying objects such as Node request, Node response, network socket, and so on. See the Koa reference.
request.location
is the parsed version of request.url
. It's very similar to a result of Node's require('url').parse
, but with location.query
parsed into a dict.
Unlike Koa, koa-ring
doesn't use prototype chains. The request is a plain JS dict. Middleware can pass modified copies:
{ return { const url = requesturl return }} { return Object}
You can override both request and response:
{ return async { request = let response = await response = return response }} { return Object}
Response
Handlers return responses. A response is a plain JS dict with the following shape:
interface Response status: number headers: {} body: any
Every field is optional. It's ok to return nothing; koa-ring
will just run the next Koa middleware.
Handlers in a middleware can easily override each other's responses:
const middleware = async { const response = await return response || status: 404}
toKoaMiddleware
Converts a koa-ring
handler into a Koa middleware. You should compose all your handlers and apply koa-ring
-style middlewares before passing the resulting handler to toKoaMiddleware
. You only need one per application.
const Koa = const toKoaMiddleware = const app = // Adds `request.body`app const echo = request app
See below for the future-based version with cancelation.
Futures
See motivation for supporting futures.
To use koa-ring
with Posterus futures and coroutines, use the optional koa-ring/posterus
module.
const Koa = const toKoaMiddleware = const app = app { const response = return response}
Routing
By preparsing request.url
into request.location
, koa-ring
makes manual routing much easier. You might not need a library:
{ const location: pathname = request if /^[/]api[/]/ return return } { const method location: pathname = request if method === 'GET' && pathname === '/api/users' return if method === 'POST' && pathname === '/api/login' return // ...}
Changelog
0.3.1
Updated to a newer version of Posterus (bugfix).
0.3.0
Breaking: replaced routing utils with URL preparsing. Tentative.
- removed
match
- removed
mount
- added
request.location
Instead of using multiple functions hidden behind routes, you're supposed to route imperatively, with a series of if/else
, by looking at the conveniently-parsed request.location
.
This is tentative, likely to be followed by more changes as I'm experimenting with the idea.
Misc
I'm receptive to suggestions. If this library almost satisfies you but needs changes, open an issue or chat me up. Contacts: https://mitranim.com/#contacts