Skip to content

Repository files navigation

vite-plugin-cloudflare-router
CI status License npm version

A file-based router for Cloudflare Workers.

Quickstart

Install

npm install -D vite-plugin-cloudflare-router

Setup

vite.config.ts

import { defineConfig } from 'vite';
import { cloudflare } from '@cloudflare/vite-plugin';
import cloudflareRouter from 'vite-plugin-cloudflare-router';

export default defineConfig({
  plugins: [
    cloudflare(),
    cloudflareRouter(),
  ],
});

tsconfig.json

{
  "extends": "./.cloudflare-router/tsconfig.json",
}

Usage

Example project structure:

.
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ routes/
β”‚   β”‚   └── index.ts      -> Route handler
β”‚   └── index.ts          -> Cloudflare Workers entry point
└── package.json

Cloudflare Workers entry point

// src/index.ts
import { routes } from 'virtual:cloudflare-router';
import { createRouter } from 'vite-plugin-cloudflare-router/runtime';

const router = createRouter(routes);

export default router;

Route handler

// src/routes/index.ts
export const GET = () => {
  return new Response('ok');
};

Routing Conventions

Every route is defined as a folder with an index.ts file.

Basic

routes/
β”œβ”€β”€ index.ts               -> /
└── api/
    └── index.ts           -> /api

Nested Routes

A . in a folder name becomes a / in the URL.

routes/
β”œβ”€β”€ index.ts               -> /
β”œβ”€β”€ api/
β”‚   └── index.ts           -> /api
└── api.users/
    └── index.ts           -> /api/users

Dynamic Segments

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

Catch-all

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)

Colocated Modules

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)

Route Handlers

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.

defineHandler

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

Middleware runs before and after the route handler. It allows you to prepare the request and post-process the response.

defineMiddleware

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;
  });

Execution Order

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



⬆️ Back to top

About

πŸ§ͺ A file-based router for Cloudflare Workers (Experimental)

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages