Skip to content

literallyJoel/router

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@literallyjoel/router

A minimal filesystem router + controller framework for Bun that:

  • Uses Bun’s native route matching (serve({ routes })) — no custom matcher on the hot path
  • Autoloads controllers from files like get.ts, post.ts, etc.
  • Validates request JSON via Standard Schema V1 (Zod, Valibot, ArkType, Yup, Joi, Effect Schema, …)
  • Optionally extracts route params from bracketed paths (e.g., /users/[id]) without replacing Bun’s router
  • Supports UUID path param validation and optional authentication
  • Returns consistent JSON errors

Standard Schema: https://standardschema.dev

Installation

bun add @literallyjoel/router
# or
npm install @literallyjoel/router

Install any Standard Schema-compatible validator you prefer (Zod, Valibot, ArkType, Yup, Joi, etc.).

Quick Start (Bun’s routes)

import { serve } from "bun";
import index from "@app/index.html";
import { getRoutes } from "@literallyjoel/router";

const routes = await getRoutes({
  routesDirectory: "./src/routes",
  routePrefix: "/api", // optional; you can also include 'api' in your directory structure
  authProvider: {
    getSession: async (headers) => {
      const token = headers.get("authorization");
      return token ? { user: { id: "123" } } : null;
    },
  },
  logger: {
    error: (message, meta) => console.error(message, meta),
  },
  [id]`
});

const server = serve({
  routes: {
    // Serve index.html for all unmatched routes.
    "/*": index,

    // Default 404 for unmatched API paths
    "/api/*": new Response("Not Found", { status: 404 }),

    // Autogenerated API endpoints from the filesystem:
    ...routes,
  },

  development: process.env.NODE_ENV !== "production" && {
    hmr: true,
    console: true,
  },

  port: 8181,
});

console.log(`🚀 Server running at ${server.url}`);

File structure

getRoutes() discovers controllers from filenames:

src/
  routes/
    users/
      get.ts      → GET /api/users
      post.ts     → POST /api/users
    users/[id]/
      get.ts      → GET /api/users/[id]
  • Use routePrefix to mount under a base path (e.g., /api).
  • Bracketed segments like [id] are recognized and, when enabled, params are extracted at runtime and attached to req.params.

Controllers

Create controllers with createController(handler, config, additionalValidator?).

  • validationSchema: any Standard Schema V1-compliant schema (Zod, Valibot, ArkType, Yup, Joi, …)
  • requiresAuthentication: boolean
  • validateUUIDs: string[] of param keys to validate as UUID
  • authProvider: override per-controller if desired (otherwise uses the one from getRoutes)

Example (Zod):

// src/routes/users/post.ts
import { createController } from "@literallyjoel/router";
import { z } from "zod";

const Schema = z.object({
  username: z.string().min(3),
  email: z.string().email(),
});

export default createController(
  async (ctrl) => {
    return Response.json({ user: ctrl.json });
  },
  {
    validationSchema: Schema, // Standard Schema via Zod
    requiresAuthentication: false,
  },
  () => []
);

Valibot:

import { createController } from "@literallyjoel/router";
import * as v from "valibot";

const Schema = v.object({
  username: v.string([v.minLength(3)]),
  email: v.string([v.email()]),
});

export default createController(
  async (ctrl) => Response.json({ user: ctrl.json }),
  { validationSchema: Schema, requiresAuthentication: false },
  () => []
);

ArkType:

import { createController } from "@literallyjoel/router";
import { type } from "arktype";

const Schema = type({ username: "string.min(3)", email: "string.email" });

export default createController(
  async (ctrl) => Response.json({ user: ctrl.json }),
  { validationSchema: Schema, requiresAuthentication: false },
  () => []
);

Params and UUIDs

  • When params extraction is enabled (default), bracketed paths like /users/[id] will populate req.params = { id: "..." }.
  • Controllers can ask to validate specific params as UUIDs:
export default createController(
  async (ctrl) => Response.json({ id: ctrl.params.userId }),
  {
    requiresAuthentication: false,
    validateUUIDs: ["userId"],
  },
  () => []
);

Notes:

  • Bun’s native routes map doesn’t populate req.params. This package adds params only for paths discovered with bracket segments, keeping the overhead minimal (compiled once at boot; O(segments) match per request for those routes only).
  • Static routes incur zero param-extraction overhead.

Authentication

Provide an authProvider in getRoutes() to enable sessions for all routes:

authProvider: {
  getSession: async (headers) => {
    const token = headers.get("authorization");
    return token ? { user: { id: "123" } } : null;
  },
}

If a controller sets requiresAuthentication: true and session is missing, the controller responds with 401 automatically.

Validation (Standard Schema)

This library accepts any validator that implements Standard Schema V1:

  • The schema must expose ~standard.validate(value) and version: 1.
  • validate returns { value } on success or { issues } on failure (sync or async).
  • Validation failures are returned as ValidationError (400) with a standardized fields array.

Example error response:

{
  "message": "Bad Request",
  "fields": [
    { "field": "email", "message": "Invalid email" }
  ]
}

Built-in errors

  • ValidationError (400)
  • UnauthorizedError (401)
  • ForbiddenError (403)
  • NotFoundError (404)
  • ConflictError (409)
  • InternalServerError (500)

Usage:

throw new NotFoundError({ message: "User not found" });

API

  • getRoutes(options)

    • routesDirectory: string
    • routePrefix?: string (prefix all discovered routes, e.g., /api)
    • authProvider?: { getSession(headers): Promise<any | null> }
    • logger?: { error(message, meta?) }
  • createController(handler, config, additionalValidator?)

    • validationSchema?: StandardSchemaV1<any, TData>
    • validateUUIDs?: string[]
    • requiresAuthentication: boolean
    • authProvider?: AuthProvider
  • BaseController

    • request: BunRequest
    • ctx: HandlerContext
    • json: TData (validated)
    • params: Record<string, string> (validated UUIDs if configured)
    • session, user

Build & Publish

bun build ./src --outdir ./dist
npm publish --access public

package.json should include:

{
  "type": "module",
  "main": "./dist/index.js",
  "exports": { ".": "./dist/index.js" }
}

Notes

  • Validation uses Standard Schema V1 (sync or async). Issues are mapped to field errors.
  • Relies on Bun’s routes map for dispatch. This package only discovers controllers and wires handlers.
  • Param extraction is runtime-only for bracketed paths; static routes have zero overhead.

About

A file-based router for Bun

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published