oakserver / oak

A middleware framework for handling HTTP with Deno, Node, Bun and Cloudflare Workers 🐿️ 🦕
https://oakserver.org
MIT License
5.2k stars 233 forks source link
deno http-server middleware-framework middleware-frameworks oak router-middleware

oak

jsr.io/@oak/oak jsr.io/@oak/oak score deno.land/x/oak npm Version

oak ci codecov

A middleware framework for Deno's native HTTP server, Deno Deploy, Node.js 16.5 and later, Cloudflare Workers and Bun. It also includes a middleware router.

This middleware framework is inspired by Koa and middleware router inspired by @koa/router.

This README focuses on the mechanics of the oak APIs and is intended for those who are familiar with JavaScript middleware frameworks like Express and Koa as well as a decent understanding of Deno. If you aren't familiar with these, please check out documentation on oakserver.github.io/oak.

Also, check out our FAQs and the awesome-oak site of community resources.

[!NOTE] The examples in this README pull from main and are designed for Deno CLI or Deno Deploy, which may not make sense to do when you are looking to actually deploy a workload. You would want to "pin" to a particular version which is compatible with the version of Deno you are using and has a fixed set of APIs you would expect. https://deno.land/x/ supports using git tags in the URL to direct you at a particular version. So to use version 13.0.0 of oak, you would want to import https://deno.land/x/oak@v13.0.0/mod.ts.

Usage

Deno CLI and Deno Deploy

oak is available on both deno.land/x and JSR. To use from deno.land/x, import into a module:

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

To use from JSR, import into a module:

import { Application } from "jsr:@oak/oak";

Or use the Deno CLI to add it to your project:

deno add jsr:@oak/oak

Node.js

oak is available for Node.js on both npm and JSR. To use from npm, install the package:

npm i @oakserver/oak

And then import into a module:

import { Application } from "@oakserver/oak";

To use from JSR, install the package:

npx jsr i @oak/oak

And then import into a module:

import { Application } from "@oak/oak/application";

[!NOTE] Send, websocket upgrades and serving over TLS/HTTPS are not currently supported.

In addition the Cloudflare Worker environment and execution context are not currently exposed to middleware.

Cloudflare Workers

oak is available for Cloudflare Workers on JSR. To use add the package to your Cloudflare Worker project:

npx jsr add @oak/oak

And then import into a module:

import { Application } from "@oak/oak/application";

Unlike other runtimes, the oak application doesn't listen for incoming requests, instead it handles worker fetch requests. A minimal example server would be:

import { Application } from "@oak/oak/application";

const app = new Application();

app.use((ctx) => {
  ctx.response.body = "Hello CFW!";
});

export default { fetch: app.fetch };

[!NOTE] Send and websocket upgrades are not currently supported.

Bun

oak is available for Bun on JSR. To use install the package:

bunx jsr i @oak/oak

And then import into a module:

import { Application } from "@oak/oak/application";

[!NOTE] Send and websocket upgrades are not currently supported.

Application, middleware, and context

The Application class coordinates managing the HTTP server, running middleware, and handling errors that occur when processing requests. Two of the methods are generally used: .use() and .listen(). Middleware is added via the .use() method and the .listen() method will start the server and start processing requests with the registered middleware.

A basic usage, responding to every request with Hello World!:

import { Application } from "jsr:@oak/oak/application";

const app = new Application();

app.use((ctx) => {
  ctx.response.body = "Hello World!";
});

await app.listen({ port: 8000 });

You would then run this script in Deno like:

> deno run --allow-net helloWorld.ts

For more information on running code under Deno, or information on how to install the Deno CLI, check out the Deno manual.

The middleware is processed as a stack, where each middleware function can control the flow of the response. When the middleware is called, it is passed a context and reference to the "next" method in the stack.

A more complex example:

import { Application } from "jsr:@oak/oak/application";

const app = new Application();

// Logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.headers.get("X-Response-Time");
  console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`);
});

// Timing
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.response.headers.set("X-Response-Time", `${ms}ms`);
});

// Hello World!
app.use((ctx) => {
  ctx.response.body = "Hello World!";
});

await app.listen({ port: 8000 });

To provide an HTTPS server, then the app.listen() options need to include the options .secure option set to true and supply a .certFile and a .keyFile options as well.

.handle() method

The .handle() method is used to process requests and receive responses without having the application manage the server aspect. This though is advanced usage and most users will want to use .listen().

The .handle() method accepts up to three arguments. The first being a Request argument, and the second being a Deno.Conn argument. The third optional argument is a flag to indicate if the request was "secure" in the sense it originated from a TLS connection to the remote client. The method resolved with a Response object or undefined if the ctx.respond === true.

An example:

import { Application } from "jsr:@oak/oak/application";

const app = new Application();

app.use((ctx) => {
  ctx.response.body = "Hello World!";
});

Deno.serve(async (request, info) => {
  const res = await app.handle(request, info.remoteAddr);
  return res ?? Response.error();
});

An instance of application has some properties as well:

Context

The context passed to middleware has several properties:

The context passed to middleware has some methods:

Unlike other middleware frameworks, context does not have a significant amount of aliases. The information about the request is only located in .request and the information about the response is only located in .response.

Cookies

The context.cookies allows access to the values of cookies in the request, and allows cookies to be set in the response. It automatically secures cookies if the .keys property is set on the application. Because .cookies uses the web crypto APIs to sign and validate cookies, and those APIs work in an asynchronous way, the cookie APIs work in an asynchronous way. It has several methods:

Request

The context.request contains information about the request. It contains several properties:

And several methods:

Request Body

[!IMPORTANT] This API changed significantly in oak v13 and later. The previous API had grown organically since oak was created in 2018 and didn't represent any other common API. The API introduced in v13 aligns better to the Fetch API's Request way of dealing with the body, and should be more familiar to developers coming to oak for the first time.

The API for the oak request .body is inspired by the Fetch API's Request but with some add functionality. The context's request.body is an instance of an object which provides several properties:

It also has several methods:

Response

The context.response contains information about the response which will be sent back to the requestor. It contains several properties:

And several methods:

Automatic response body handling

When the response Content-Type is not set in the headers of the .response, oak will automatically try to determine the appropriate Content-Type. First it will look at .response.type. If assigned, it will try to resolve the appropriate media type based on treating the value of .type as either the media type, or resolving the media type based on an extension. For example if .type was set to "html", then the Content-Type will be set to "text/html".

If .type is not set with a value, then oak will inspect the value of .response.body. If the value is a string, then oak will check to see if the string looks like HTML, if so, Content-Type will be set to text/html otherwise it will be set to text/plain. If the value is an object, other than a Uint8Array, a Deno.Reader, or null, the object will be passed to JSON.stringify() and the Content-Type will be set to application/json.

If the type of body is a number, bigint or symbol, it will be coerced to a string and treated as text.

If the value of body is a function, the function will be called with no arguments. If the return value of the function is promise like, that will be await, and the resolved value will be processed as above. If the value is not promise like, it will be processed as above.

Opening the server

The application method .listen() is used to open the server, start listening for requests, and processing the registered middleware for each request. This method returns a promise when the server closes.

Once the server is open, before it starts processing requests, the application will fire a "listen" event, which can be listened for via the .addEventListener() method. For example:

import { Application } from "jsr:@oak/oak/application";

const app = new Application();

app.addEventListener("listen", ({ hostname, port, secure }) => {
  console.log(
    `Listening on: ${secure ? "https://" : "http://"}${
      hostname ?? "localhost"
    }:${port}`,
  );
});

// register some middleware

await app.listen({ port: 80 });

Closing the server

If you want to close the application, the application supports the option of an abort signal. Here is an example of using the signal:

import { Application } from "jsr:@oak/oak/application";

const app = new Application();

const controller = new AbortController();
const { signal } = controller;

// Add some middleware using `app.use()`

const listenPromise = app.listen({ port: 8000, signal });

// In order to close the server...
controller.abort();

// Listen will stop listening for requests and the promise will resolve...
await listenPromise;
// and you can do something after the close to shutdown

Error handling

Middleware can be used to handle other errors with middleware. Awaiting other middleware to execute while trapping errors works. So if you had an error handling middleware that provides a well managed response to errors would work like this:

import { Application } from "jsr:@oak/oak/application";
import { isHttpError } from "jsr:@oak/commons/http_errors";
import { Status } from "jsr:@oak/commons/status";

const app = new Application();

app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    if (isHttpError(err)) {
      switch (err.status) {
        case Status.NotFound:
          // handle NotFound
          break;
        default:
          // handle other statuses
      }
    } else {
      // rethrow if you can't handle the error
      throw err;
    }
  }
});

Uncaught middleware exceptions will be caught by the application. Application extends the global EventTarget in Deno, and when uncaught errors occur in the middleware or sending of responses, an EventError will be dispatched to the application. To listen for these errors, you would add an event handler to the application instance:

import { Application } from "jsr:@oak/oak/application";

const app = new Application();

app.addEventListener("error", (evt) => {
  // Will log the thrown error to the console.
  console.log(evt.error);
});

app.use((ctx) => {
  // Will throw a 500 on every request.
  ctx.throw(500);
});

await app.listen({ port: 80 });

Router

The Router class produces middleware which can be used with an Application to enable routing based on the pathname of the request.

Basic usage

The following example serves up a RESTful service of a map of books, where http://localhost:8000/book/ will return an array of books and http://localhost:8000/book/1 would return the book with ID "1":

import { Application } from "jsr:@oak/oak/application";
import { Router } from "jsr:@oak/oak/router";

const books = new Map<string, any>();
books.set("1", {
  id: "1",
  title: "The Hound of the Baskervilles",
  author: "Conan Doyle, Arthur",
});

const router = new Router();
router
  .get("/", (context) => {
    context.response.body = "Hello world!";
  })
  .get("/book", (context) => {
    context.response.body = Array.from(books.values());
  })
  .get("/book/:id", (context) => {
    if (books.has(context?.params?.id)) {
      context.response.body = books.get(context.params.id);
    }
  });

const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });

A route passed is converted to a regular expression using path-to-regexp, which means parameters expressed in the pattern will be converted. path-to-regexp has advanced usage which can create complex patterns which can be used for matching. Check out the documentation for that library if you have advanced use cases.

In most cases, the type of context.params is automatically inferred from the path template string through typescript magic. In more complex scenarios this might not yield the correct result however. In that case you can override the type with router.get<RouteParams>, where RouteParams is the explicit type for context.params.

Nested routers

Nesting routers is supported. The following example responds to http://localhost:8000/forums/oak/posts and http://localhost:8000/forums/oak/posts/nested-routers.

import { Application } from "jsr:@oak/oak/application";
import { Router } from "jsr:@oak/oak/router";

const posts = new Router()
  .get("/", (ctx) => {
    ctx.response.body = `Forum: ${ctx.params.forumId}`;
  })
  .get("/:postId", (ctx) => {
    ctx.response.body =
      `Forum: ${ctx.params.forumId}, Post: ${ctx.params.postId}`;
  });

const forums = new Router().use(
  "/forums/:forumId/posts",
  posts.routes(),
  posts.allowedMethods(),
);

await new Application().use(forums.routes()).listen({ port: 8000 });

Static content

The function send() is designed to serve static content as part of a middleware function. In the most straight forward usage, a root is provided and requests provided to the function are fulfilled with files from the local file system relative to the root from the requested path.

A basic usage would look something like this:

import { Application } from "jsr:@oak/oak/application";

const app = new Application();

app.use(async (context, next) => {
  try {
    await context.send({
      root: `${Deno.cwd()}/examples/static`,
      index: "index.html",
    });
  } catch {
    await next();
  }
});

await app.listen({ port: 8000 });

send() automatically supports features like providing ETag and Last-Modified headers in the response as well as processing If-None-Match and If-Modified-Since headers in the request. This means when serving up static content, clients will be able to rely upon their cached versions of assets instead of re-downloading them.

ETag support

The send() method automatically supports generating an ETag header for static assets. The header allows the client to determine if it needs to re-download an asset or not, but it can be useful to calculate ETags for other scenarios.

There is a middleware function that assesses the context.reponse.body and determines if it can create an ETag header for that body type, and if so sets the ETag header on the response. Basic usage would look something like this:

import { Application } from "jsr:@oak/oak/application";
import { factory } from "jsr:@oak/oak/etag";

const app = new Application();

app.use(factory());

// ... other middleware for the application

There is also a function which retrieves an entity for a given context based on what it logical to read into memory which can be passed to the etag calculate that is part of the Deno std library:

import { Application } from "jsr:@oak/oak/application";
import { getEntity } from "jsr:@oak/oak/etag";
import { calculate } from "jsr:@std/http/etag";

const app = new Application();

// The context.response.body has already been set...

app.use(async (ctx) => {
  const entity = await getEntity(ctx);
  if (entity) {
    const etag = await calculate(entity);
  }
});

Fetch API and Deno.serve() migration

If you are migrating from Deno.serve() or adapting code that is designed for the web standard Fetch API Request and Response, there are a couple features of oak to assist.

ctx.request.source

When running under Deno, this will be set to a Fetch API Request, giving direct access to the original request.

ctx.response.with()

This method will accept a Fetch API Response or create a new response based on the provided BodyInit and ResponseInit. This will also finalize the response and ignores anything that may have been set on the oak .response.

middleware/serve#serve() and middelware/serve#route()

These two middleware generators can be used to adapt code that operates more like the Deno.serve() in that it provides a Fetch API Request and expects the handler to resolve with a Fetch API Response.

An example of using serve() with Application.prototype.use():

import { Application } from "jsr:@oak/oak/application";
import { serve } from "jsr:@oak/oak/serve";

const app = new Application();

app.use(serve((req, ctx) => {
  console.log(req.url);
  return new Response("Hello world!");
}));

app.listen();

And a similar solution works with route() where the context contains the information about the router, like the params:

import { Application } from "jsr:@oak/oak/application";
import { Router } from "jsr:@oak/oak/router";
import { route } from "jsr:@oak/oak/serve";

const app = new Application;

const router = new Router();

router.get("/books/:id", route((req, ctx) => {
  console.log(ctx.params.id);
  return Response.json({ title: "hello world", id: ctx.params.id });
}));

app.use(router.routes());

app.listen();

Testing

The mod.ts exports an object named testing which contains some utilities for testing oak middleware you might create. See the Testing with oak for more information.


There are several modules that are directly adapted from other modules. They have preserved their individual licenses and copyrights. All of the modules, including those directly adapted are licensed under the MIT License.

All additional work is copyright 2018 - 2024 the oak authors. All rights reserved.