Webroute

Routes

A webroute is simply definition for an endpoint. That definition can include methods, a path, middleware, input/output validation and much more. This is useful because it means each route is self-sufficient. In turn, this opens up many opportunities for keeping code simple, more robust, more type-safe and more portable across runtimes, frameworks and platforms.

Webroutes are created using the route builder from @webroute/route. The route builder is very much a utility that can be introduced into existing projects to build more powerful routes.

// npm i @webroute/route
import { route } from "@webroute/route";
 
const myRoute = route().handle(() => {
  return new Response("OK");
});

In this trivial example, the myRoute export is a web-standard request handler, in the form (request: Request) => Response. This function signature enables us to use our routes nearly anywhere.

Overview

At a minimum, a route should have a handler – a function that processes the incoming request. However, we can specify several more properties which enrich the information about and functionality of our routes.

  • HTTP info: .path(), .method()
  • Handling: .handle()
  • Schema validation: .params(), .query, .body(), .headers(), .output()
  • Middleware: .use()
  • Dependency injection: .provide()

Example Usage

route("/user/:id")
  .use(isAuthed)
  .params(z.object({ id: z.number({ coerce: true }) }))
  .handle(async (req, c) => {
    const userId = c.state.userId; // Provided by isAuthed
 
    return { userId }; // Will be JSON.stringified
  });

Here we are using auth middleware, specifying path param schema and handling our request.

Immutability

The routes themselves are immutable. So every time we chain a method, we are creating a new instance which is entirely isolated. This enables powerful patterns like composition.

Immutability also means the following will not work:

const myRoute = route("");
 
myRoute.params(Params); // <- This will not affect `myRoute`

HTTP Path and Method

Every HTTP endpoint has a path and a method. By defining these, we easily wire up routes to external routers, create OpenAPI specs and end-to-end type-safe clients.

Depending on how your app is deployed, you may not require setting these properties but it can still be helpful.

Route Path

A route path can be defined when initialising a new route.

const myRoute = route("/foo");

It can also be specified explicitly via .path().

route().path("/foo");

Bear in mind, paths are appended, which enables path prefixing.

const apiRoute = route("/api");
 
const myRoute = apiRoute.path("/foo");
// -> /api/foo

We can also define path parameters. The style of path parameter adheres to the web URLPattern API, which is essentially the express-like syntax familiar to most.

route("/user/:id");

Route Method(s)

We can similarly define methods for our path. However, specifying new .methods will override previous declarations. We can define methods in several ways.

route().method("get"); // lowercase string
route().method("GET"); // uppercase string
route().method(["get", "post"]); // array of many methods

Request Handling

As we have seen, our actual request handling logic exists is specified via the .handle() method. Request handlers look something like

route().handle(() => {
  // Handling logic
});

Parameters

Request handlers always receive two arguments.

The Incoming Request

The incoming Request provides all the incoming information sent by some client. It arrives to the request handler untouched, unchanged. Any changes must be applied via...

The Request Context

The second parameter is the webroute request context. It provides context-dependent functionality and is specific to that route.

route().handle((req, c) => {
  // ...
});

Return Type

Request handlers can return raw data, or a Response.

Returning Data

If we return data, it is implied the data is of JSON type, and a Response will be invisibly created as such. The data is JSON.stringifyd and the Response has Content-Type: application/json.

Returning a Response

While returning raw data is handy, we often require customising the Response status or headers. In this case, we should initialise and return a Reponse.

	.handle(() => {
		return new Response("OK", { status: 201 })
	});

Request State

Often, requests require state to be propagated through the request pipeline. For example, middleware may wish to add some data to be used by downstream handlers.

Instead of simply placing state on the Request itself, webroute provides the more explicit .state property on the request context.

route().handle((req, c) => {
  const userId = c.state.userId;
});

State can also be strongly typed, which makes our applications more robust by avoiding accessing incorrect or missing data.

You can learn more about updating state in the Middleware docs.

Route Utilities

The Route utility, as opposed to the lowercase route, can help extract information about our routes. This includes the path and methods, as well as any type information, like query or param types.

import { Route } from "@webroute/route";

Route Values

const path = Route.getPath(myRoute);
const methods = Route.getMethods(myRoute);
const operations = Route.getOperationKeys(myRoute);

Route Types

We can infer all parts of a route definition.

type RouteDef = Route.InferRouteDef<typeof myRoute>;

Or by individual part.

type Path = Route.InferPath<typeof myRoute>;
type Methods = Route.InferMethods<typeof myRoute>;
 
// In = before transform
type ParamsIn = Route.InferParamsIn<typeof myRoute>;
type QueryIn = Route.InferQueryIn<typeof myRoute>;
// ...etc
 
// Out = after transform (if any)
type ParamsOut = route.InferParamsOut<typeof myRoute>;
type QueryOut = Route.InferQueryOut<typeof myRoute>;
// ...etc

Additional Reading

Schema Validation

Our routes can also perform powerful, type-safe schema validation. For example:

Read the validation docs.

Middleware

Complex request pipelines can be created simply by using middleware and composition.

View the middleware guide.

Dependency Injection

Some prefer to use dependency injection to invert control. Basic dependency injection is possible using the .provide() method.

Learn more about Dependency Injection.

On this page