Webroute is a set of building blocks which can combine to form a fully fledged web framework. Each tool is decoupled from the next, meaning they are all independently useful. But they can also be stacked, leaving you with a strong foundation for quickly building backend web apps that are more future-proofed.
View the documentation.
Given the immense range of how modern apps are deployed, from microservices to monoliths to serverless, Webroute has been designed to be selectively adopted, only using the relevant parts to your situation.
Webroute can be used standalone, slot into existing apps or integrated alongside other frameworks, without awkwardness.
- âś… Hono
- âś… NextJS
- âś… Remix
- âś… SolidStart
- âś… Bun
- âś… Deno
- âś… Node (via adapter)
- âś… Cloudflare Workers
- âś… Vercel Edge
Webroute provides several packages that are entirely independent of one another. Combined, they can be used to create fully-fledged apps.
They will work with any framework or runtime that utilises web-standard Request
and Response
objects.
The @webroute/route
package primarily exports the route
builder. This enables building routes which support declaring and composing input/output schema, headers, middleware and paths all at once. The result is a single standard request handler which can be used anywhere.
export const GET = route()
.use(someMiddleware)
.params(ParamsSchema)
.handle(() => {
/**...*/
});
@webroute/client
provides helpers for calling APIs on the client-side with full type-safety. This reduces errors and expedites frontend development.
Unlike most client-side API helpers, @webroute/client
is agnostic to what tech you're running on the backend. It will work with any REST API regardless of framework, runtime or language.
// Define endpoints here
type App = DefineApp<{
// GET /posts?limit=X
"GET /posts": {
Query: { limit?: number };
};
}>;
const client = createTypedClient<App>({
fetcher: async (
config,
opts: AxiosRequestConfig // Provide custom input parameters
) => {
return axios(/**...*/); // and result type
},
});
// All of the below is fully type-safe
const getPost = client("GET /post/:id");
const axiosRes = getPost({ id: 123 }, axiosOpts);
@webroute/middleware
is a tiny helper for defining framework-agnostic middleware.
Middleware implementations will be added to separate packages in the future.
@webroute/router
provides minimal web routers which merely match incoming request to suitable handlers of any form.
const router = createRadixRouter(routes);
const handler = router.match(request);
return handler(request);
@webroute/schema
bridges the gaps between modern schema/validation libraries. It' provides interfaces and implementations for converting from schema to e.g. JSON schema, or even to another schema library.
const parser = createParser(ZodParser());
const formatter = createFormatter(JoiFormatter());
const myZodSchema = z.object({ foo: z.number() });
const parsed = parser.parse(myZodSchema);
// E.g. convert to Joi
const myJoiSchema = formatter.format(parsed);
@webroute/oas
provides utilities for creating OpenAPI Specs with any API. It provides an OAS
utility which helps to decorate API schema and operations with OpenAPI-relevant metadata.
When paired with @webroute/schema
, OpenAPI specs can be derived while using any modern schema library.
import { OAS, createSpec } from "@webroute/oas";
const schemaWithMeta = OAS.Schema(z.object({}), { description: "..." });
const spec = createSpec([
{
path: "/foo",
methods: ["get"],
Query: OAS.Param(schemaWithMeta, parameterSpecificConfig),
},
]);
To understand why webroute was developed, we should first look at the issues or drawbacks with the current approaches.
const app = new FrameworkApp();
app.use((req, res, next) => {
req.doSomething();
next();
});
app.get("/posts/:id", (req, res) => res.send("..."));
This example is what you might see in a typical JS web framework. While familiar, and simple to write, it has several problems. We've largely become numb to these problems, but that doesn't mean they stop being problems.
Note
The point of this section is not to disparage the existing tools.
Each framework tends to create it's own Request
and Response
abstraction. When middleware or other tooling builds on this basis, it immediately becomes coupled to the given framework. Consquently, the reach of it's utility is diminished, and the next framework requires a reimplementation.
For end developers creating backend applications, this additionally means they must learn the ins, outs and various edge cases or idiosyncracies for this particular implementation. This adds mental overhead and often results in unexpected issues. For example, express
will error if you res.send
after middleware has already sent the headers
, which is not remotely obvious.
This leads us to the second issue.
Using approaches like the above, where middleware is defined somewhere and handlers somewhere else, makes it difficult to answer the question: "what code is actually executed during this request?".
These approaches use the framework, and more specifically it's router, as the source of truth for orchestration. But when our API recieves a request it has a single endpoint, a single destination. Following this logic instead, our route handlers should really be the "true" basis or source of truth for what happens. Additionally, by deferring execution to a router, instead of just using the programming language itself to run the code, the ability for us (and our compiler) to associate related code is greatly diminished, making e.g. type-safety impossible.
In typical web frameworks, middleware are granted the ability to imperatively orchestrate the app. Given middleware is often non-owned code (third party), this further obscures what's going on under the hood. In actuality, middleware operates in such a way that this affordance is often not worth it.
Instead, webroute inverts the responsibility for orchestration. Middleware is only responsible for doing work, and returns values that indicate a suggestion for what should happen next. Instead of next()
, middleware can return data, a Response
or a response handler, to indicate whether it has computed some data or state, wants to terminate the request early, or do some post-processing on the outgoing Response
.
Web frameworks frequently employ mutability as the central mechanism for communication and effecting change. Once again, this makes it more difficult to determine what is happening, or to create robust systems.
Instead, webroute prefers a kind of "functional pipeline" approach, where data flows through various functions which can offer to update that data. It is loosely immutable, meaning it's not truly immutable, but leans into immutability as the primary mechanism, over mutating data.
Every piece of functionality, thus becomes a producer, instead of a mutator. The end user can then decide what to do with the "produce", if anything. Consequently, our system becomes much more observable and trivially customiseable.
Aside from the bespokeness of the Request
and Response
objects, described above, most frameworks additionally suffer from not leaning into standards. Web standards are a relatively new and evolving standard which provide an opportunity for more standardised tooling too, if we choose to sieze it.
All of these improvements over the traditional approaches enable much more observable and reliable backends. Perhaps even more importantly, any code we (or others) write has a much longer lifetime since it is decoupled from any framework, and built on web-standards.
Feel free to open any PRs or issues or start a discussion.
Webroute was initially developed by Nick Sinclair