JSPM

astro-routify

1.2.2
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 2
  • Score
    100M100P100Q47231F
  • License MIT

A high-performance API router for Astro using a Trie-based matcher.

Package Exports

  • astro-routify

Readme

astro-routify

A high-performance API router for Astro built on a Trie matcher.
Define API routes using clean, flat structures — no folders or boilerplate logic.

npm license downloads feedback-welcome


Installing

npm install astro-routify

⚡️ Quickstart

// 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:

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 repository. It showcases a minimal Astro app with API endpoints configured under:

/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 for recent updates and improvements.


🧠 Core Concepts

defineRoute()

Declare a single route:

defineRoute(HttpMethod.GET, "/users/:id", ({params}) => {
    return ok({userId: params.id});
});

defineRouter()

Group multiple routes under one HTTP method handler:

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.

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:

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(...)):

import {fileResponse} from 'astro-routify';

ok(data);                   // 200 OK
created(data);              // 201 Created
noContent();                // 204
notFound("Missing");        // 404
internalError(err);         // 500

📄 File downloads

fileResponse(content, "application/pdf", "report.pdf"); // sets Content-Type and Content-Disposition

🔄 Streaming responses

Raw stream (e.g., Server-Sent Events)

stream('/clock', async ({response}) => {
    const timer = setInterval(() => {
        response.write(new Date().toISOString());
    }, 1000);

    setTimeout(() => {
        clearInterval(timer);
        response.close();
    }, 5000);
});

JSON NDStream (newline-delimited)

streamJsonND('/updates', async ({response}) => {
    response.send({step: 1});
    await delay(500);
    response.send({step: 2});
    response.close();
});

JSON Array stream

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:

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

// 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

// 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

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


☕ Support

If this project helps you, consider buying me a coffee. Every drop keeps the code flowing!