Eva

A lightweight, dependency-free HTTP framework for Bun, built from scratch in TypeScript.

Quick start

import { Eva } from './src/eva';
import { cors } from './src/cors';

const app = new Eva();

// Global middleware
app.use(cors({ origin: '*' }));

// Routes
app.get('/users/:id', (ctx) => {
  return ctx.toJson({ id: ctx.params.id });
});

app.serve(3000);

Features

Feature Description
Trie routing Per-method prefix tree with params, optional params and a trailing wildcard.
Middleware Global and route-level, may short-circuit with a Response.
Built-in CORS Exact-match origins (string, array or *), preflight handled.
Error boundary Typed EvaError hierarchy mapped to HTTP status codes.
Context Lazy, cached access to params, query and the request body.
Cookies Parse the Cookie header into ctx.getCookies(); set Set-Cookie via ctx.setCookie().
Body limit Middleware capping request body size; replies 413 over the limit.
HEAD & 405 Automatic HEAD on any GET route; 405 with an Allow header.

Live routes

GET routes are clickable. Others need a tool (curl / Postman).

Method Route Description
GET /health Plain text ok + custom X-Eva-Version header
GET /echo/:id Route param echoed back as JSON
GET /search Query string returned as JSON
GET /wildcard/* Wildcard captures the rest of the path
GET /users/:id JSON user — id 999 throws a 404
GET /boom Throws — exercises the 500 error boundary
GET /admin Needs header Authorization: secret — else 401
GET /old-path 301 redirect to /
GET /tasks List tasks
POST /tasks Create from a JSON array body — max 5 items, 100 KB body limit
GET /cookies Echoes incoming cookies and sets a session cookie
POST /items Echoes the JSON body back with status 201
GET /api/v1/products Mounted child instance — product list
GET /api/v1/products/:id Single product by id

API reference

Eva

app.get(route, handler);                 // also post, put, patch, delete, options
app.get(route, ...middlewares, handler); // route-level middleware (handler last)
app.route(route).get(h).post(h);         // group methods on one path

app.use(middleware);                     // global middleware
app.onError(handler);                    // error boundary
app.serve(port?, callback?);             // start Bun server, returns it

Context

ctx.params;                  // route params (percent-decoded)
ctx.query;                   // query string object
ctx.req;                     // original Request
await ctx.json();            // parse request body as JSON (cached)
await ctx.text();            // raw request body (cached)
ctx.getHeader(name);         // read a request header
ctx.getCookies();            // parsed request cookies (cached)
ctx.setHeader(name, value);  // set a response header
ctx.appendHeader(name, val); // append a response header (multi-value)
ctx.setCookie({ name, value, options }); // add a Set-Cookie header

ctx.toJson(data, opts);      // JSON response
ctx.toText(text, opts);      // text response
ctx.redirect(url, status);   // redirect (default 301)
ctx.notFound();              // 404 response

Middleware

type EvaMiddleware = (
  ctx: EvaContext,
  next: () => Promise<void>,
) => Promise<Response | void>;

// Return a Response to short-circuit; call next() to continue the chain.

Architecture

Request
  →  Global middleware chain
  →  Route matching (trie)
  →  Route-level middleware
  →  Handler
  →  Response
        (errors → EvaError status, or onError, or a generic 500)

Testing

bun test            # run the suite
bun test --watch    # re-run on change
bun test --coverage # coverage report

Tests live next to the code as *.test.ts and use bun:test — zero config, Jest-compatible.