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.