A type-safe, lightweight router designed specifically for Cloudflare Workers with built-in JSON:API support. Inspired by itty-router and SvelteKit.
- 🔥 Zero dependencies - Lightweight and fast
- 🛡️ Type-safe - Full TypeScript support with route parameter inference
- 🌐 Cloudflare Workers optimized - Built for the edge
- 📡 JSON:API compliant - Follows JSON:API specification
- 🚀 Simple API - Intuitive method chaining
- 🔄 Dynamic routes - Support for parameters, wildcards, and catch-all routes
- ⚡ High performance - Minimal overhead with efficient route matching
- 🛠️ Optional response helpers - Built-in helpers for common responses and errors
npm install @agilearn/router
import { router, json, error, notFound, internalServerError } from '@agilearn/router';
const app = router();
// Simple route
app.get('/hello', () => json({ message: 'Hello, World!' }));
// Route with parameters
app.get('/users/:id', ({ params }) => {
return json({
data: {
type: 'users',
id: params.id,
attributes: { name: 'John Doe' },
},
});
});
// Handle not found
app.get('/missing', () => notFound());
// Handle errors
app.post('/users', () => {
try {
// Some operation that might fail
throw new Error('Database error');
} catch (err) {
return internalServerError(err);
}
});
export default app;
The router includes several built-in response helpers for common HTTP responses:
import { json, created } from '@agilearn/router/response';
// JSON response with data (200 OK)
json({ message: 'Success' });
// JSON with custom status and headers
created(
{ data: user },
{
headers: { Location: '/users/123' },
}
);
import { error } from '@agilearn/router';
// Basic error (follows JSON:API error format)
error(404, 'Not Found');
// Error with description
error(400, 'Bad Request', 'Invalid email format');
// Error with Error object
error(500, 'Internal Server Error', new Error('Database failed'));
import { notFound, internalServerError } from '@agilearn/router/response';
// 404 Not Found
app.get('/missing', () => notFound());
// 500 Internal Server Error
app.get('/error', () => internalServerError());
// 500 with custom error
app.get('/custom-error', () => internalServerError(new Error('Custom error')));
const app = router<Options>({
base?: string;
onError?: (error: unknown, request: Request) => Response | Promise<Response>;
});
Options:
-
base
- Base path for all routes (optional). Must start with '/'. -
onError
- Custom error handler (optional, defaults tointernalServerError
)
The router supports all standard HTTP methods:
app.get(path, ...handlers);
app.post(path, ...handlers);
app.put(path, ...handlers);
app.patch(path, ...handlers);
app.delete(path, ...handlers);
app.head(path, ...handlers);
app.options(path, ...handlers);
app.trace(path, ...handlers);
app.get('/users', handler);
app.post('/api/v1/posts', handler);
// Single parameter
app.get('/users/:id', ({ params }) => {
console.log(params.id); // Type-safe!
});
// Multiple parameters
app.get('/users/:userId/posts/:postId', ({ params }) => {
console.log(params.userId, params.postId);
});
// Matches everything after /files/
app.get('/files/:path+', ({ params }) => {
console.log(params.path); // Could be "docs/readme.txt"
});
// Matches any path
app.get('/api/*', handler);
type RequestHandler<Options, Pathname> = (
event: RequestEvent<Options, Pathname>
) => Response | Promise<Response> | void;
interface RequestEvent<Options, Pathname> {
params: RouteParams<Pathname>; // Route parameters (type-safe)
platform: Platform<Options>; // Cloudflare Workers context
request: Request; // Original request
url: URL; // Parsed URL
}
interface MyEnv {
DATABASE_URL: string;
API_KEY: string;
}
interface MyProps {
userId: string;
}
const app = router<{
env: MyEnv;
props: MyProps;
}>();
app.get('/protected', ({ platform }) => {
// platform.env is typed as MyEnv
// platform.ctx.props is typed as MyProps
const dbUrl = platform.env.DATABASE_URL;
return json({ success: true });
});
import { internalServerError } from '@agilearn/router/response';
const app = router({
onError: (error, request) => {
console.error('Router error:', error);
return internalServerError(error);
},
});
import { error } from '@agilearn/router/response';
const authenticate = ({ request, platform }) => {
const token = request.headers.get('Authorization');
if (!token) {
throw error(401, 'Unauthorized');
}
// Continue to next handler by returning void/undefined
};
const getUser = ({ params }) => {
return json({
data: {
type: 'users',
id: params.id,
},
});
};
// Chain handlers
app.get('/users/:id', authenticate, getUser);
import { badRequest, internalServerError, notFound } from '@agilearn/router';
app.get('/users/:id', async ({ params, platform }) => {
try {
const user = await getUser(params.id, platform.env.DATABASE_URL);
if (!user) {
return notFound();
}
return json({
data: {
type: 'users',
id: user.id,
attributes: user,
},
});
} catch (err) {
if (err instanceof ValidationError) {
return badRequest(err.message);
}
return internalServerError(err);
}
});
This router is designed for JSON:API applications and includes:
- Structured error responses following JSON:API error object format
- Support for JSON:API document structure
// JSON:API compliant response
app.get('/users/:id', ({ params }) => {
return json(
{
data: {
type: 'users',
id: params.id,
attributes: {
name: 'John Doe',
email: 'john@example.com',
},
},
},
{
headers: { 'Content-Type': 'application/vnd.api+json' },
}
);
});
// wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"
# src/index.ts
import { json, router } from '@agilearn/router';
const app = router();
app.get('/', () => json({ message: 'Hello World!' }));
export default app;
Then deploy:
npx wrangler deploy
MIT
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.