Skip to content

routup

A minimalistic, runtime-agnostic HTTP framework. Return-based handlers, Web Standards everywhere — Node, Bun, Deno, Cloudflare, or any Fetch-ready runtime.

MIT licensed · Node 22+ · ESM-only · TypeScript-first

import { App, defineCoreHandler, serve } from 'routup';

const app = new App();

app.get('/users/:id', defineCoreHandler(
    (event) => ({ id: Number(event.params.id) })
));

serve(app, { port: 3000 });
app.fetch(request)

Registered routes

  • GET/userslist users
  • GET/users/:idshow user
  • POST/userscreate user
  • GET/files/*splatserve file

Send a request

live

Booting app.fetch()

Each keystroke re-runs app.fetch() against a real Request. Same model that ships to Node, Bun, Deno, and Cloudflare Workers.

Six pieces. One small core.

Routup keeps the surface narrow — App, Handler, Event — and lets the runtime, the Web platform, and tree-shaking do the rest.

🌐

Web Standards

Built on srvx — every handler receives a real Request and returns whatever toResponse() can convert: string, object, stream, Response.

🏃‍♂️

Runtime Agnostic

Conditional exports auto-pick the right adapter for Node, Bun, Deno, Cloudflare Workers, Service Workers, or any Fetch-ready runtime.

🚀

Return-based handlers

No res.send(). Return a value; the pipeline picks the Content-Type, sets ETag/304, and ships the response — async or sync, your call.

🧅

Onion middleware

Call event.next() to wrap downstream handlers. Mutate event.response before, transform the resolved value after.

🔌

Plugins & hooks

Install reusable plugins with name + version + dependencies. Subscribe to request, response, and error lifecycle events.

🌳

Tree-shakeable helpers

Tiny standalone functions — getRequestIP, sendRedirect, setResponseCacheHeaders. Import only what you use; the rest is gone.

From zero to first request

Three steps. No decorators, no schema DSL, no per-runtime build target.

npm install routup

routup/node

Drop in alongside Express.

Two helpers turn routup into an incremental migration. Wrap an existing compression(), cors(), or passport.authenticate() chain with fromNodeHandler(). Mount the router inside any http.Server with toNodeHandler().

  • fromNodeHandler() — wrap any (req, res, next) middleware as a routup handler
  • toNodeHandler() — convert an App to a Node-style (req, res) handler
  • No rewrite — Express middleware keeps working while you port routes one at a time
  • Same App everywhere — the same routes ship to Bun, Deno, and Cloudflare unchanged
Read the migration guide →
migrate.ts
// migrate.ts
import http from 'node:http';
import compression from 'compression';
import { App, defineCoreHandler } from 'routup';
import { toNodeHandler, fromNodeHandler } from 'routup/node';

const app = new App();

// 1. Wrap an Express/Connect middleware as a routup handler.
app.use(fromNodeHandler(compression()));

// 2. Add a return-based handler alongside it.
app.get('/health', defineCoreHandler(() => ({ ok: true })));

// 3. Mount the whole app inside a plain Node http.Server.
http.createServer(toNodeHandler(app)).listen(3000);