oak
A middleware framework for Deno's http server, including a router middleware.
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.
Warning The examples in this README pull from master
, 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 3.0.0 of oak, you would want to import
https://deno.land/x/oak@v3.0.0/mod.ts
.
Application, middleware, and context
The Application
class wraps the serve()
function from the http
package. It
has two methods: .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 "https://deno.land/x/oak/mod.ts";
const app = new Application();
app.use((ctx) => {
ctx.response.body = "Hello World!";
});
await app.listen({ port: 8000 });
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 "https://deno.land/x/oak/mod.ts";
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.
An instance of application has some properties as well:
.keys
Keys to be used when signing and verifying cookies. The value can be set to an array of keys, and instance of
KeyStack
, or an object which provides the same interface asKeyStack
(e.g. an instance of keygrip). If just the keys are passed, oak will manage the keys viaKeyStack
which allows easy key rotation without requiring re-signing of data values..state
A record of application state, which can be strongly typed by specifying a generic argument when constructing an
Application()
, or inferred by passing a state object (e.g.Application({ state })
).
Context
The context passed to middleware has several properties:
.app
A reference to the
Application
that is invoking this middleware..cookies
The
Cookies
instance for this context which allows you to read and set cookies..request
The
Request
object which contains details about the request..response
The
Response
object which will be used to form the response sent back to the requestor..state
A record of application state, which can be strongly typed by specifying a generic argument when constructing an
Application()
, or inferred by passing a state object (e.g.Application({ state })
).
The context passed to middleware has two methods:
.assert()
Makes an assertion, which if not true, throws an
HTTPError
, which subclass is identified by the second argument, with the message being the third argument..throw()
Throws an
HTTPError
, which subclass is identified by the first argument, with the message being passed as the second.
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. It has several methods:
.get(key: string, options?: CookieGetOptions)
Attempts to retrieve the cookie out of the request and returns the value of the cookie based on the key. If the applications
.keys
is set, then the cookie will be verified against a signed version of the cookie. If the cookie is valid, the value will be returned. If it is invalid, the cookie signature will be set to deleted on the response. If the cookie was not signed by the current key, it will be resigned and added to the response..set(key: string, value: string, options?: CookieSetDeleteOptions)
Will set a cookie in the response based on the provided key, value and any options. If the applications
.keys
is set, then the cookie will be signed and the signature added to the response.
Request
The context.request
contains information about the request. It contains
several properties:
.hasBody
Set to
true
if the request has a body, orfalse
if it does not. It does not validate if the body is supported by the built in body parser though..headers
The headers for the request, an instance of
Headers
..method
A string that represents the HTTP method for the request.
.secure
A shortcut for
.protocol
, returningtrue
if HTTPS otherwisefalse
..serverRequest
The original
net
server request..url
An instance of
URL
which is based on the full URL for the request. This is in place of having parts of the URL exposed on the rest of the request object.
And several methods:
.accepts(...types: string[])
Negotiates the content type supported by the request for the response. If no content types are passed, the method returns a prioritized array of accepted content types. If content types are passed, the best negotiated content type is returned. If there is no content type matched, then
undefined
is returned..acceptsCharsets(...charsets: string[])
Negotiates the character encoding supported by the request for the response. If no character encodings are passed, the method returns a prioritized array of accepted character encodings. If character encodings are passed, the best negotiated charset is returned. If there are no encodings matched, then
undefined
is returned.Most browsers simply to not send a character encoding header anymore, and it is just expected UTF-8 will be used.
.acceptsEncodings(...encodings: string[])
Negotiates the content encoding supported by the request for the response. If no encodings are passed, the method returns a prioritized array of accepted encodings. If encodings are passed, the best negotiated encoding is returned. If there are no encodings matched, then
undefined
is returned..acceptsLanguages(...languages: string[])
Negotiates the language the client is able to understand. Where a locale variant takes preference. If no encodings are passed, the method returns a prioritized array of understood languages. If languages are passed, the best negotiated language is returned. If there are no languages matched, then
undefined
is returned..body(options?: BodyOptions)
The method resolves to a version of the request body. Currently oak supports request body types of JSON, text and URL encoded form data. If the content type is missing, the request will be rejected with a 415 HTTP Error.
When the option
asReader
is false or not passed, the method resolves with an object which contains atype
property set to"json"
,"text"
,"form"
,"undefined"
, or"raw"
and avalue
property set with the parsed value of the property. For JSON it will be the parsed value of the JSON string. For text, it will simply be a string and for a form, it will be an instance ofURLSearchParams
. For an undefined body, the value will beundefined
. If the content type is not supported, the body will be returned with atype
of"raw"
and thevalue
will be set to aUint8Array
containing the raw bytes for the request. If the application cannot handle the content type, it should throw a 415 HTTP Error.When option
asReader
is true, the method resolves with an object who'stype
property is"reader"
and who'svalue
property is aDeno.Reader
which is the HTTP server request's native response.You can use the option
contentTypes
to set additional media types that when present as the content type for the request, the body will be parsed accordingly. The options takes possibly four keys:json
,form
,text
, andraw
. For example if you wanted JavaScript sent to the server to be parsed as text, you would do something like this:app.use((ctx) => { const result = await ctx.request.body({ contentTypes: { text: ["application/javascript"], }, }); result.type; // "text" result.value; // a string containing the text });
In particular the
contentTypes.raw
can be used to override default types that are supported that you would want the middleware to handle itself. For example if you wanted the middleware to parse all text media types itself, you would do something like this:app.use((ctx) => { const result = await ctx.request.body({ contentTypes: { raw: ["text"], }, }); result.type; // "raw" result.value; // a Uint8Array of all of the bytes read from the request });
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
or null
, the object will be passed to JSON.stringify()
and the Content-Type
will be set to application/json
.
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 "https://deno.land/x/oak/mod.ts";
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 sever...
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,
isHttpError,
Status,
} from "https://deno.land/x/oak/mod.ts";
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 "https://deno.land/x/oak/mod.ts";
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, Router } from "https://deno.land/x/oak/mod.ts";
const books = new Map<string, any>();
books.set("1", {
id: "1",
title: "The Hound of the Baskervilles",
author: "Conan Doyle, Author",
});
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 (context.params && context.params.id && 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 });
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, send } from "https://deno.land/x/oak/mod.ts";
const app = new Application();
app.use(async (context) => {
await send(context, context.request.url.pathname, {
root: `${Deno.cwd()}/examples/static`,
index: "index.html",
});
});
await app.listen({ port: 8000 });
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 - 2020 the oak authors. All rights reserved.