astro-routify
Version:
A high-performance API router for Astro using a Trie-based matcher.
368 lines (258 loc) β’ 8.88 kB
Markdown
# astro-routify
**A high-performance API router for [Astro](https://astro.build/) built on a Trie matcher.**
Define API routes using clean, flat structures β no folders or boilerplate logic.




## Installing
```shell
npm install astro-routify
```
## β‘οΈ Quickstart
```ts
// src/pages/api/index.ts
import {
defineRoute,
defineRouter,
defineGroup,
HttpMethod,
ok,
} from 'astro-routify';
const userGroup = defineGroup('/users', (group) => {
group.addGet('/:id', ({params}) => ok({id: params.id}));
});
export const GET = defineRouter([
defineRoute(HttpMethod.GET, '/ping', () => ok('pong')),
...userGroup.getRoutes(),
]);
```
Or to handle everything in a single place:
```ts
import {RouterBuilder, ok} from 'astro-routify';
const builder = new RouterBuilder();
builder
.addGet('/ping', () => ok('pong'))
.addPost('/submit', async ({request}) => {
const body = await request.json();
return ok({received: body});
});
export const ALL = builder.build(); // catch-all
```
## π‘ Full Example
You can find an implementation example in the [astro-routify-example](https://github.com/oamm/astro-routify-example)
repository.
It showcases a minimal Astro app with API endpoints configured under:
```text
/src/pages/api/[...path].ts
```
This setup demonstrates how to route requests dynamically using astro-routify, while still leveraging Astro's native
endpoint system.
## π Features
- β‘ Fully compatible with Astroβs native APIContext β no extra setup needed.
- π§© Use middleware, access cookies, headers, and request bodies exactly as you would in a normal Astro endpoint.
- β
Flat-file, code-based routing (no folders required)
- β
Dynamic segments (`:id`)
- β
ALL-mode for monolithic routing (`RouterBuilder`)
- β
Built-in response helpers (`ok`, `created`, etc.)
- β
Trie-based matcher for fast route lookup
- β
Fully typed β no magic strings
- π **Streaming support**
- `stream()` β raw streaming with backpressure support (e.g. SSE, logs, custom protocols)
- `streamJsonND()` β newline-delimited JSON streaming (NDJSON)
- `streamJsonArray()` β server-side streamed JSON arrays
> π See [CHANGELOG.md](./CHANGELOG.md) for recent updates and improvements.
## π§ Core Concepts
### `defineRoute()`
Declare a single route:
```ts
defineRoute(HttpMethod.GET, "/users/:id", ({params}) => {
return ok({userId: params.id});
});
```
### `defineRouter()`
Group multiple routes under one HTTP method handler:
```ts
export const GET = defineRouter([
defineRoute(HttpMethod.GET, "/health", () => ok("ok"))
]);
```
> π§ `defineRouter()` supports all HTTP methods β but Astro only executes the method you export (`GET`, `POST`, etc.)
### `RouterBuilder` (Catch-All & Fluent Builder)
Use `RouterBuilder` when you want to build routes dynamically, catch all HTTP methods via `ALL`, or organize routes more
fluently with helpers.
```ts
const builder = new RouterBuilder();
builder
.addGet("/ping", () => ok("pong"))
.addPost("/submit", async ({request}) => {
const body = await request.json();
return ok({received: body});
});
export const ALL = builder.build();
```
You can also group routes:
```ts
const users = defineGroup("/users")
.addGet("/:id", ({params}) => ok({id: params.id}));
builder.addGroup(users);
```
> π While `.register()` is still available, it's **deprecated** in favor of `.addGroup()` and `.addRoute()` for better
> structure and reusability.
## π Response Helpers
Avoid boilerplate `new Response(JSON.stringify(...))`:
```ts
import {fileResponse} from 'astro-routify';
ok(data); // 200 OK
created(data); // 201 Created
noContent(); // 204
notFound("Missing"); // 404
internalError(err); // 500
```
### π File downloads
```ts
fileResponse(content, "application/pdf", "report.pdf"); // sets Content-Type and Content-Disposition
```
### π Streaming responses
#### Raw stream (e.g., Server-Sent Events)
```ts
stream('/clock', async ({response}) => {
const timer = setInterval(() => {
response.write(new Date().toISOString());
}, 1000);
setTimeout(() => {
clearInterval(timer);
response.close();
}, 5000);
});
```
#### JSON NDStream (newline-delimited)
```ts
streamJsonND('/updates', async ({response}) => {
response.send({step: 1});
await delay(500);
response.send({step: 2});
response.close();
});
```
#### JSON Array stream
```ts
streamJsonArray('/items', async ({response}) => {
for (let i = 0; i < 3; i++) {
response.send({id: i});
}
response.close();
});
```
## π Param Matching
Any route param like `:id` is extracted into `ctx.params`:
```ts
const builder = new RouterBuilder();
builder.addGet("/users/:id", ({params}) => ok({userId: params.id}));
//OR
defineRoute(HttpMethod.GET, "/items/:id", ({params}) => {
return ok({itemId: params.id});
});
```
## π€― Why Use astro-routify?
### β Without it
```ts
// src/pages/api/[...slug].ts
export const GET = async ({request}) => {
const url = new URL(request.url);
const path = url.pathname;
if (path.startsWith('/api/users/')) {
// Try to extract ID
const id = path.split('/').pop();
return new Response(JSON.stringify({id}), {
status: 200,
headers: {'Content-Type': 'application/json'},
});
}
if (path === '/api/users') {
return new Response(JSON.stringify([{id: 1}, {id: 2}]), {
status: 200,
headers: {'Content-Type': 'application/json'},
});
}
if (path === '/api/ping') {
return new Response(JSON.stringify({pong: true}), {
status: 200,
headers: {'Content-Type': 'application/json'}
});
}
return new Response('Not Found', {status: 404});
};
```
### π And then there's folder hell...
```
src/
ββ pages/
β ββ api/
β β ββ users/
β β β ββ index.ts // GET all users
β β β ββ [id]/
β β β β ββ index.ts // GET / POST / DELETE for a user
β β ββ ping.ts
```
### β
With `astro-routify`
```ts
// src/pages/api/[...slug].ts
const builder = new RouterBuilder();
builder.addGet("/ping", () => ok({pong: true}));
builder.addGet("/users/:id", ({params}) => ok({userId: params.id}));
// OR
export const ALL = defineRouter([
defineRoute(HttpMethod.GET, "/ping", () => ok({pong: true})),
defineRoute(HttpMethod.GET, "/users/:id", ({params}) => ok({id: params.id}))
]);
```
## π Performance
`astro-routify` uses a Trie structure for fast route and method matching.
Itβs optimized for real-world route hierarchies, and avoids nested `if` chains.
## π§ͺ Benchmarks
Realistic and synthetic benchmarks using `vitest bench`.
### π₯ Benchmark Machine
Tests ran on a mid-range development setup:
- **CPU**: Intel Core i5-7600K @ 3.80GHz (4 cores)
- **RAM**: 16 GB DDR4
- **GPU**: NVIDIA GeForce GTX 1080 (8 GB)
- **OS**: Windows 10 Pro 64-bit
- **Node.js**: v20.x
- **Benchmark Tool**: [Vitest Bench](https://vitest.dev/guide/features.html#benchmarking)
Results may vary slightly on different hardware.
### π¬ Realistic route shapes (5000 registered routes):
```
β RouteTrie performance - realistic route shapes
Β· Static route lookup (5000) 1,819,681 req/sec
Β· Param route: /users/:userId 1,708,264 req/sec
Β· Nested param route: /users/:id/orders/:oid 1,326,324 req/sec
Β· Blog route: /blog/:year/:month/:slug 1,220,882 req/sec
Β· Nonexistent path 1,621,934 req/sec
```
### π Route scaling test:
```
β RouteTrie performance
Β· Lookup in SMALL (100 routes) 1,948,385 req/sec
Β· Lookup in MEDIUM (1000 routes) 1,877,248 req/sec
Β· Lookup in LARGE (10000 routes) 1,908,279 req/sec
Β· Lookup non-existent route in LARGE 1,962,051 req/sec
```
> β‘ Performance stays consistently fast even with 10k+ routes
## π Designed to Scale
While focused on simplicity and speed today, `astro-routify` is designed to evolve β enabling more advanced routing
patterns in the future.
## π License
MIT β Β© 2025 [Alex Mora](https://github.com/oamm)
## β Support
If this project helps you, consider [buying me a coffee](https://coff.ee/alex_mora). Every drop keeps the code flowing!