Webroute

Quick Start

The guide will leave you with both a decent understanding of webroute and a starting point to build a real-world app. The resulting app will have the following:

  • End-to-end type-safety
  • Automatic OpenAPI spec generation
  • Input/output validation
  • Authentication middleware
  • Compatibility with many runtimes and frameworks

To get started immediately, you can bootstrap a standalone webroute app via:

npx create-webroute-app ./my-app

Creating API routes

1. Define a Route

To begin, we'll create a single POST route for creating new blog posts.

// npm i @webroute/route
import {  } from "@webroute/route";
 
const  = async () => {
  // Create post somehow...
  return { : "123" };
};
 
const  = ("/post")
  .("post")
  .(() => {
    return ();
  });

This basic route, createPostRoute, is a web-standard request handler which can be run nearly anywhere, including Next.js API routes, within a Hono app or as part of a standalone Bun or Node app.

// Regular web request handler...
const response = createPostRoute(new Request(/**...*/));

But our route isn't doing anything interesting yet!

2. Add Input Validation

Validating the inputs and outputs coming in and out of our server is a critical component of any serious backend application. Webroute's offer first-class support for specifying validation schema, supporting all popular schema libraries.

// Define a schema/validator, e.g. Zod
const  = .({
  : .(),
});
 
const  = ("/post")
  .("post")
  .()
  .(async (, ) => {
    const {  } = await .();
 
    return ();
  });

By specifying our schema up front, our routes more declarative and enable OpenAPI spec generation, type-inference and end-to-end type-safety, among other things.

We can also specify schema for path .params(), .query() params, .headers() and response .output()s.

Validating inputs

Bear in mind, inputs are only parsed upon calling the .parse() method, to enable greater flexibility with error handling.

Using Middleware

Middleware is a common part of any web framework. In webroute, return values are used to update request state, return early with a response or listen to and modify outgoing responses.

1. Define Auth Middleware

We want to ensure only logged in users can create posts. Since we expect this to be a fairly common use case, we can use middleware and composition to avoid unnecessary repititon.

// npm i @webroute/middleware
import { defineMiddleware } from "@webroute/middleware";
 
export const isAuthed = () =>
  defineMiddleware((request) => {
    const bearer = request.headers.get("Bearer");
    const token = bearer?.replace("Bearer ", "");
 
    if (token == null || !isValid(token)) {
      return Response.json({ code: "UNAUTHORIZED" }, { status: 401 });
    }
 
    return { token };
  });

Our middleware simply returns any state updates and requires no bespoke API calls like next(), nor can we modify the immutable request object.

Learn more about what can be achieved by different middleware return types.

defineMiddleware is optional

Note that defineMiddleware is not doing anything special under the hood. It is used to ensure your middleware has compatible parameter and return types.

2. Register Middleware on Routes

To register this with a route, we can call the .use() method. We can also – optionally – use composition to create a reusable base route for any future authed routes.

const authedRoute = route().use(isAuthed());
 
// Extend from authedRoute
const createPostRoute = authedRoute.path("/post").handle((req, c) => {
  const { token } = c.state;
});
// ...snip

The .state property of our requet context, c, houses all the data returned from any middleware on the way to our request handler. Because webroutes are immutable, we can reuse our base routes and maintain independent state between them. By using this pattern, our middleware is automatically type-safe with no extra work.

Routing

Most of the time, we have multiple routes that we want to map an incoming request to. In other words, we require routing of some sort.

Filesystem Routing

In filesystem routing contexts like Next.js, we can just export our routes as-is from route handler files (or their equivalent).

Learn more about integrating with full-stack frameworks.

Manual Routing

For explicit routing, we can use a router from the @webroute/router package to help us map requests to the various route handlers we have.

First, we should collect all of our routes into a single object.

routes.ts
export const AppRoutes = {
  createPostRoute,
};
main.ts
// npm i @webroute/router
import { createRadixRouter } from "@webroute/router";
import { Route } from "@webroute/route";
import { AppRoutes } from "./routes";
 
// Creates an array of { path, method, payload } for AppRoutes
const routeList = Object.values(AppRoutes).map(Route.normalise);
 
const router = createRadixRouter(routeList);

This router allows us to match handler based on an incoming request. Therefore, our router can also be used anywhere web-standard requests are supported.

const handleRequest = async (request: Request) => {
  const handler = router.match(request);
 
  // If no match, 404
  if (handler == null) {
    return Response.json({ code: "NOT_FOUND" }, { status: 404 });
  }
 
  // Otherwise try to handle request
  try {
    return handler(request); // Handlers will always return Response
  } catch (e) {
    // Handle unhandled exceptions here
    console.warn(e);
    return Response.json({ code: "INTERNAL_SERVER_ERROR" }, { status: 500 });
  }
};

This root request handler is, in essence, our "app". It is the entrypoint of our API. Exactly how we use this entrypoint depends on the framework or runtime we decide to use.

Next Steps

To understand how to run your webroute app, check out one of the many integration guides.

Full-stack Frameworks

Standalone

Web Frameworks

More Functionality

Webroute also provides additional packages for further enhancing or interacting with APIs.

  • Use @webroute/client to create typed API clients for any API
  • Use @webroute/oas to define and create OpenAPI specs

On this page