uai-router
v0.1.2
Published
Minimal Bun-native HTTP framework. Express-style routing, WebSocket, GraphQL.
Readme
uai-router
Minimal Bun-native HTTP framework. Express-style routing, WebSocket, and GraphQL — zero abstractions, no polyfills.
import { Router, cors, logger } from 'uai-router'
const app = new Router()
.use(cors({ origin: '*' }))
.use(logger())
.get('/users/:id', (req, ctx) => Response.json({ id: ctx.params.id! }))
.post('/users', authMiddleware, async (req, ctx) => {
const body = await req.json()
return Response.json(body, { status: 201 })
})
.ws('/chat', { open(ws) { ws.send('connected!') } })
.graphql('/graphql', { schema, resolvers })
Bun.serve({ fetch: app.fetch(), websocket: app.websocket() })Features
- Express-style —
.get(path, ...middlewares, handler),.use(mw),.use('/path', router) - WebSocket — built-in, no
wspackage needed - GraphQL — built-in via
.graphql()with optional GraphiQL - AI — built-in via
.ai()using Vercel AI SDK with streaming - Auto-REST — instant CRUD from any SQLite/Postgres URL via
.resources() - Bulk & Stats — transactional batch ops and aggregation queries
- Type-safe —
defineMiddleware<T>()for ctx extension - Fast — trie-based routing, ~57k req/s on a simple route
- Zero deps — only
graphql,@graphql-tools/schema(optional), and Vercel AI SDK (optional)
Quick Start
import { Router } from 'uai-router'
const router = new Router()
.get('/', () => Response.json({ hello: 'world' }))
const server = Bun.serve({ fetch: router.fetch() })API
Route Methods
.get(path, ...middlewares, handler)
.post(path, ...middlewares, handler)
.put(path, ...middlewares, handler)
.delete(path, ...middlewares, handler)
.patch(path, ...middlewares, handler)
.head(path, ...middlewares, handler)
.options(path, ...middlewares, handler)
.all(path, ...middlewares, handler) // matches any method
.route(method, path, ...middlewares, handler)Middleware
// Global
.use((req, ctx, next) => {
console.log(`${req.method} ${new URL(req.url).pathname}`)
return next(req, ctx)
})
// Path-scoped
.use('/api', authMiddleware)
// Sub-router
.use('/api', apiRouter)
// Per-route
.get('/admin', authMiddleware, adminHandler)Error Handling
.onError((err, req, ctx) =>
Response.json({ error: err.message }, { status: 500 }),
)
.onWsError((err, ws, ctx) => console.error('WS error:', err))Type-safe Context
import { defineMiddleware, defineRoute } from 'uai-router'
const auth = defineMiddleware<{ user: { name: string } }>((req, ctx, next) => {
ctx.user = { name: req.headers.get('Authorization') ?? 'guest' }
return next(req, ctx)
})
const handler = defineRoute<{ user: { name: string } }>((req, ctx) =>
Response.json({ name: ctx.user.name }),
)
router.get('/profile', auth, handler)Or augment the global context:
declare module 'uai-router' {
interface Context {
user?: { name: string }
}
}Auth
import { auth } from 'uai-router'
// Custom verification (JWT, session, DB lookup, etc.)
const mw = auth({
verify: async (token) => {
const user = await db.findUserByToken(token)
return user ? { sub: user.id, role: user.role } : null
}
})
router.get('/admin', mw, adminHandler)
// Proxy validation — mirrors the request to an external auth service
// Header token → proxy receives Authorization: Bearer <token>
// Query token → proxy receives ?access_token=<token>
router.get('/protected', auth({
proxy: 'http://auth-service:3000/validate'
}), handler)
// Extend the payload type:
declare module 'uai-router' {
interface AuthPayload {
sub?: string
role?: string
}
}Token resolution: Authorization: Bearer <token> → ?access_token=<token>.
WebSocket
.ws('/ws', {
open(ws, ctx) { ws.send('connected') },
message(ws, ctx, data) { ws.send(`echo: ${data}`) },
close(ws, ctx) { console.log('disconnected') },
})GraphQL
.graphql('/graphql', {
schema: `type Query { hello: String }`,
resolvers: { Query: { hello: () => 'world' } },
graphiql: true, // enables GraphiQL UI at GET /graphql
})Resources (Auto-REST CRUD)
Auto-generate a REST API for any table from a SQLite or PostgreSQL connection string.
import { resources } from 'uai-router'
const router = new Router().use('/api', resources('sqlite://data.db'))
// Creates: GET/POST /api/:table, GET/PUT/PATCH/DELETE /api/:table/:idOr with custom SQL adapter:
import { createResources } from 'uai-router'
import { SQL } from 'bun'
const resourcesRouter = createResources({
db: new SQL('sqlite://data.db'),
adapter: sqliteAdapter, // or postgresAdapter
})Routes
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/:table | List records (with filters, pagination) |
| POST | /api/:table | Create single or batch records |
| GET | /api/:table/:id | Get record by ID |
| PUT | /api/:table/:id | Replace record |
| PATCH | /api/:table/:id | Partial update |
| DELETE | /api/:table/:id | Delete record |
| POST | /api/:table/_bulk | Bulk operations |
| GET | /api/:table/_stats | Aggregation stats |
Filtering
Use query parameters with operator suffixes. Supports chaining multiple filters.
GET /api/users?age_gt=18&status_eq=active&name_contains=alice| Operator | Example | Behavior |
|----------|---------|----------|
| _eq | name_eq=Alice | Equals (default) |
| _ne | name_ne=Bob | Not equals |
| _gt | age_gt=18 | Greater than |
| _gte | age_gte=18 | Greater than or equal |
| _lt | age_lt=65 | Less than |
| _lte | age_lte=65 | Less than or equal |
| _btw | age_btw=18,65 | Between |
| _like | name_like=%son% | SQL LIKE |
| _contains | name_contains=son | Contains substring |
| _starts | name_starts=al | Starts with |
| _ends | name_ends=son | Ends with |
| _in | id_in=1,2,3 | In list |
| _nin | id_nin=4,5 | Not in list |
| _null | email_null=true | Is null |
| _notnull | email_notnull=true | Is not null |
Logical filters via POST body:
POST /api/users/_search
{
"filter": ["and", [
{ "age_gt": 18 },
["or", [
{ "status_eq": "active" },
{ "role_eq": "admin" }
]]
]]
}Pagination
GET /api/users?page=1&pageSize=20&sort=name&order=asc
GET /api/users?limit=10&offset=0
GET /api/users?cursor=100&pageSize=20Response includes { data, total, page?, pageSize?, _cursor?, _hasMore? }.
Bulk Operations
POST /api/orders/_bulk
Content-Type: application/json
[
{ "op": "create", "table": "orders", "data": { "item": "Widget", "qty": 1 } },
{ "op": "update", "table": "orders", "ids": [1], "data": { "qty": 5 } },
{ "op": "delete", "table": "orders", "filter": { "status_eq": "cancelled" } },
{ "op": "patch", "table": "orders", "ids": [2], "data": { "status": "shipped" } }
]All operations run in a single transaction.
Statistics
GET /api/users/_stats?age_avg=true&age_min=true&age_max=true&role_by=trueResponse: { total: 100, age: { _avg: 34.5, _min: 18, _max: 87 }, role: { _groups: [...] } }
Schema
Data is stored in a unified resources table with columns: id, table_name, data (JSON), created_at, updated_at. The data column holds all record fields as JSON. The schema is auto-created on first use.
AI (Vercel AI SDK)
import { ollama } from 'ollama-ai-provider-v2'
.ai('/chat', async (req) => {
const { messages } = await req.json()
return {
model: ollama('qwen3:0.6b'),
messages,
}
})Or use OpenAI/Anthropic via provider packages:
import { openai } from '@ai-sdk/openai'
import { anthropic } from '@ai-sdk/anthropic'
.ai('/chat', async (req) => {
const { prompt } = await req.json()
return {
model: openai('gpt-4o'),
prompt,
}
})With tools (function calling):
import { z } from 'zod'
import { tool } from 'uai-router'
.ai('/assistant', async (req) => {
const { messages } = await req.json()
return {
model: ollama('qwen3:0.6b'),
messages,
tools: {
get_weather: tool({
description: 'Get the current weather for a city',
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => {
return { temperature: 72, conditions: 'sunny', city }
},
}),
},
}
})License
MIT
