rtr
A fast HTTP router and type-safe middleware library for Fetch compliant runtimes.
Usage
These examples below use Deno, but they could be easily adapted for runtimes that support Request
/Response
(many edge runtimes, Bun, etc.)
Handlers
// a handler is a function that takes a request and context, and returns a response
type Handler<T extends object> = (
request: Request,
context: T,
) => Response | Promise<Response>;
const greeting: Handler<{ name: string }> = (req, ctx) => {
return new Response(`Hello ${ctx.name}!`);
}
const asyncHandler: Handler<{ userid: string }> = async (req, ctx) => {
const user = await findById(ctx.userid);
return new Response(`Hello ${user.name}!`);
}
Routing
import { serve } from "https://deno.land/std/http/server.ts";
import { Handler, Router, ParamsCtx } from "https://deno.land/x/rtr/mod.ts";
const router = new Router();
// you can use parameters in the path
router.get("/user/:userid", async (req: Request, ctx: ParamsCtx) => {
// access the path parameters with the `params` field of the context
const user = await findById(ctx.params.userid);
return new Response(JSON.stringify(user));
});
// of course paths can have multiple parameters
router.delete("/user/:userid/todos/:todoId", async (req: Request, ctx: ParamsCtx) => {
await deleteTodo(ctx.params.userId, ctx.params.todoId);
return new Response('OK');
})
// wildcards are supported at the end of a path
router.get("/static/*", (req: Request) => {
return serveFile(new URL(req.url).pathname);
})
// there are default implementations for 404, 405, and 500 error handling
// but they can be overridden for custom functionality
router.notFound = () => new Response("You must be lost", { status: 404 });
router.methodNotAllowed = () => new Response("That isn't allowed", { status: 405 });
router.internalServerError = () => new Response("Oops I messed up", { status: 500 });
// router.handler is a `Handler<object>` that routes the request to the paths bound above
serve(router.handler);
Middleware
// a middleware is a function that takes a handler and returns another handler
type Middleware<
Requires extends object = object,
Provides extends object = object,
> = <T extends object>(
h: Handler<T & Requires & Provides>,
) => Handler<T & Requires>;
// a logging middleware that requires a user object in context
const log: Middleware<{ user: { id: string } }> = (h) => async (r, c) => {
const id = c.user.id;
const res = await h(r, c);
console.log(`${r.method} ${r.url} ${id}`);
return res;
};
// an authorization middleware that provides a user object in context
const auth: Middleware<{}, { user: { id: string } }> = (h) => async (r, c) => {
const user = await validate(r.headers.get("Authorization"));
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
return h(r, { ...c, user });
};
// you can compose middleware
const middleware = composeMiddleware(auth, log);
// which is the same as
const middleware = (h) => auth(log(h));
// and then apply the middleware to handlers
router.get('/asdf', middleware((_, ctx) => new Response('asdf: ' + ctx.user.id)));