A file-based router for Cloudflare Workers.
npm install -D vite-plugin-cloudflare-router
import { defineConfig } from 'vite';
import { cloudflare } from '@cloudflare/vite-plugin';
import cloudflareRouter from 'vite-plugin-cloudflare-router';
export default defineConfig({
plugins: [
cloudflare(),
cloudflareRouter(),
],
});{
"extends": "./.cloudflare-router/tsconfig.json",
}Example project structure:
.
βββ src/
β βββ routes/
β β βββ index.ts -> Route handler
β βββ index.ts -> Cloudflare Workers entry point
βββ package.json
// src/index.ts
import { routes } from 'virtual:cloudflare-router';
import { createRouter } from 'vite-plugin-cloudflare-router/runtime';
const router = createRouter(routes);
export default router;// src/routes/index.ts
export const GET = () => {
return new Response('ok');
};Every route is defined as a folder with an index.ts file.
routes/
βββ index.ts -> /
βββ api/
βββ index.ts -> /api
A . in a folder name becomes a / in the URL.
routes/
βββ index.ts -> /
βββ api/
β βββ index.ts -> /api
βββ api.users/
βββ index.ts -> /api/users
Wrap a parameter name with [] to make a dynamic segment.
routes/
βββ index.ts -> /
βββ api/
β βββ index.ts -> /api
βββ api.users/
β βββ index.ts -> /api/users
βββ api.users.[id]/
βββ index.ts -> /api/users/:id
Use [...] in a folder name to catch the rest of the path.
routes/
βββ index.ts -> /
βββ api/
β βββ index.ts -> /api
βββ api.users/
β βββ index.ts -> /api/users
βββ api.users.[id]/
β βββ index.ts -> /api/users/:id
βββ api.docs.[...slug]/
βββ index.ts -> /api/docs/* (catch-all)
Only the index.ts file directly under the route folder (e.g. /api.users.[id]/index.ts) is a route. Any other file β including files in subfolders β is a colocated module, not a route.
routes/
βββ index.ts -> /
βββ api/
β βββ index.ts -> /api
βββ api.users/
β βββ index.ts -> /api/users
βββ api.users.[id]/
β βββ index.ts -> /api/users/:id
β βββ shared/
β βββ index.ts -> colocated module
β βββ schema.ts -> colocated module
βββ api.docs.[...slug]/
βββ index.ts -> /api/docs/* (catch-all)
To handle an incoming request, export a function named after the HTTP method.
export const GET = () => {};
export const POST = () => {};
export const PATCH = () => {};
export const PUT = () => {};
export const DELETE = () => {};A route handler receives one argument with the following properties:
request: The incoming HTTP request.env: The bindings available to the Worker.executionContext: The Worker's execution context.params: Parameters for the route.
// e.g. GET /api/users/123
export const GET = (c: {
request: Request;
env: Env;
executionContext: ExecutionContext;
params: { id: string };
}) => {
return new Response(c.params.id); // '123'
};The example above is a bare-bones route handler, use defineHandler instead.
To define a route handler, use defineHandler().
defineHandler infers types for you, including dynamic params. .handle() takes the same arguments as above.
import { defineHandler } from './+types';
export const GET = defineHandler()
.handle((c) => {
return new Response(c.params.id);
});Note that the import specifier is ./+types. This plugin auto-generates types when running a Vite dev server.
Middleware runs before and after the route handler. It allows you to prepare the request and post-process the response.
To create a middleware, use defineMiddleware(). Call next() to continue to the next middleware or route handler.
import { defineMiddleware } from './+types';
export const middleware = defineMiddleware()
.handle((_c, next) => {
return next();
});To use middleware, use .use() method.
export const GET = defineHandler()
.use(middleware)
.handle(() => {
return new Response('ok');
});To apply multiple middlewares, chain the .use() method.
export const GET = defineHandler()
.use(middleware1)
.use(middleware2)
.use(middleware3)
.handle(() => {
return new Response('ok');
});next() accepts arbitrary data as an argument. This data will be merged onto the handler's context and passed down to the route handler.
const middleware = defineMiddleware()
.handle((_c, next) => {
return next({ message: 'ok' });
});
export const GET = defineHandler()
.use(middleware)
.handle((c) => {
return new Response(c.message); // 'ok'
});A middleware must always return a response.
To continue the chain, return next().
To post-process the response, await the response from next() and then return it.
const middleware = defineMiddleware()
.handle(async (c, next) => {
// Simply return
return await next();
// or
const response = await next();
// post-process the response
// e.g. response.headers.set('x-custom-header', 'hello');
// and then return the response
return response;
});Middleware runs in the order in which they were registered.
const middleware1 = defineMiddleware()
.handle(async (c, next) => {
console.log('Middleware 1 start');
const response = await next();
console.log('Middleware 1 end');
return response;
});
const middleware2 = defineMiddleware()
.handle(async (c, next) => {
console.log('Middleware 2 start');
const response = await next();
console.log('Middleware 2 end');
return response;
});
const middleware3 = defineMiddleware()
.handle(async (c, next) => {
console.log('Middleware 3 start');
const response = await next();
console.log('Middleware 3 end');
return response;
});
export const GET = defineHandler()
.use(middleware1)
.use(middleware2)
.use(middleware3)
.handle((c) => {
console.log('Handler called');
return new Response('ok');
});Output:
Middleware 1 start
Middleware 2 start
Middleware 3 start
Handler called
Middleware 3 end
Middleware 2 end
Middleware 1 end