Custom Oak middleware in Deno

Mayank C
Tech Tonic

--

Introduction

Oak is a very popular middleware framework for Deno’s native HTTP server, Deno Deploy, and Node.js 16.5 and later. Oak also comes with a middleware router. Oak’s middleware framework is inspired by Koa and middleware router inspired by Koa’s router.

Oak is designed with Deno in mind, and versions of oak are tagged for specific versions of Deno in mind.

Oak is all about middlewares. A middleware is useful for a variety of cases, but especially useful in enhancing with common functionality that is applicable across all the routes. Some middleware examples are:

  • measuring response time
  • logging request and responses
  • etc.

Oak has some built-in middlewares, also there are middlewares developed by people for Oak. If there is no middleware ready for use, we can write our own easily.

In this article, we’ll learn how to write our own middleware. We’ll use request ID as an example middleware. The middleware will be developed for a very simple hello world application.

import { Application } from "https://deno.land/x/oak/mod.ts";const app = new Application();app.use((ctx) => {
ctx.response.body = "Hello world!";
});
await app.listen({ port: 8000 });

Basics of middleware

To write a middleware, first we need to import the Middleware interface from Oak:

import { Application, Middleware } from "https://deno.land/x/oak/mod.ts";

The Middleware interface is very simple, with two inputs:

  • context: The usual Oak context object
  • next: The API to call the next middleware in the chain. The call to next API depends on the functionality of the middleware. Some middlewares call it first and some in-between.
export interface Middleware<
S extends State = Record<string, any>,
T extends Context = Context<S>,
> {
(context: T, next: () => Promise<unknown>): Promise<unknown> | unknown;
}

The return value of the middleware is not important. To persist any data, the middleware sets it in the context state or the response object of the context.

The order of middleware is very important. Common middlewares like adding a request ID needs to be placed before any other middlewares.

Here is the code of the hello world application, with a middleware that simply prints the request URL & time on the console.

import { Application, Middleware } from "https://deno.land/x/oak/mod.ts";const app = new Application();const logMiddleware: Middleware = (ctx, next) => {
console.log(`Received ${ctx.request.url} at ${new Date()}`);
next();
};
app.use(logMiddleware);app.use((ctx) => {
ctx.response.body = "Hello world!";
});
await app.listen({ port: 8080 });

Here is a quick run:

> curl http://localhost:8080
Hello world!
> deno run --allow-net app.ts
Received http://localhost:8080/ at Sat Jul 30 2022 22:28:20 GMT-0700 (Pacific Daylight Time)

Works perfectly! This simple middleware adds a console line for each request.

That’s all about the basics. Let’s move on to writing request ID middleware.

Request ID middleware

It is very common to allocate a request ID for every incoming request. This request ID has the following features:

  • A unique ID is allocated for each request
  • The unique ID is printed in all the logs (local or remote)
  • The unique ID is sent back to the caller in HTTP response

The request ID is very useful in debugging failed API calls.

An Oak middleware is a great place to add a request ID to the context state & in the response headers. A random ID will be created and added as a state KV to the context object. The next API will be called. Once the next API finishes, the request ID is added to the response header so that it goes to the caller.

Here is the code of the updated application that uses a request ID middleware. This middleware is placed before the logging middleware that we’ve added in the last section. The logging middleware is also changed to print request ID.

import { Application, Middleware } from "https://deno.land/x/oak/mod.ts";const REQ_ID_HDR = "X-Request-ID";const app = new Application();const idMiddleware: Middleware = async (ctx, next) => {
const reqId = crypto.randomUUID();
ctx.state[REQ_ID_HDR] = reqId;
await next();
ctx.response.headers.set(REQ_ID_HDR, reqId);
};
const logMiddleware: Middleware = (ctx, next) => {
console.log(
`Received ${ctx.request.url} at ${new Date()} with ${
ctx.state[REQ_ID_HDR]
}`,
);
next();
};
app.use(idMiddleware);
app.use(logMiddleware);
app.use((ctx) => {
ctx.response.body = "Hello world!";
});
await app.listen({ port: 8080 });

Here is a quick run:

> curl http://localhost:8080 -v
> GET / HTTP/1.1
< HTTP/1.1 200 OK
< content-type: text/plain; charset=utf-8
< x-request-id: 45aaac0e-6ece-4038-a0e6-b334991f5dfe
Hello world!
> deno run --allow-net app.ts
Received http://localhost:8080/ at Sat Jul 30 2022 22:39:43 GMT-0700 (Pacific Daylight Time) with 45aaac0e-6ece-4038-a0e6-b334991f5dfe

The request ID middleware works perfectly!

That’s all about writing your own middlewares for Oak. As we’ve seen, it’s pretty easy to write a middleware. The important part is where to place it.

--

--