rouzer
v1.0.0-beta.14
Published
Type-safe routes shared by your server and client, powered by `zod/mini` (input validation + transforms), `@remix-run/route-pattern` (URL matching), and `alien-middleware` (typed middleware chaining). The router output is intended to be used with `@hattip
Readme
rouzer
Type-safe routes shared by your server and client, powered by zod/mini (input validation + transforms), @remix-run/route-pattern (URL matching), and alien-middleware (typed middleware chaining). The router output is intended to be used with @hattip/core adapters.
Install
pnpm add rouzer zodEverything is imported directly from rouzer.
Define routes (shared)
// routes.ts
import * as z from 'zod/mini'
import { $type, route } from 'rouzer'
export const helloRoute = route('hello/:name', {
GET: {
query: z.object({
excited: z.optional(z.boolean()),
}),
// The response is only type-checked at compile time.
response: $type<{ message: string }>(),
},
})
export const routes = { helloRoute }The following request parts can be validated with Zod:
pathquerybodyheaders
Zod validation happens on both the server and client.
Route URL patterns
Rouzer uses @remix-run/route-pattern for matching and generation. Patterns can include:
- Pathname-only patterns like
blog/:slug(default). - Full URLs with protocol/hostname/port like
https://:store.shopify.com/orders. - Dynamic segments with
:paramnames (valid JS identifiers), including multiple params in one segment likev:major.:minor. - Optional segments wrapped in parentheses, which can be nested like
api(/v:major(.:minor)). - Wildcards with
*name(captured) or*(uncaptured) for multi-segment paths likeassets/*pathorfiles/*. - Query matching with
?to require parameters or exact values likesearch?qorsearch?q=routing.
Server router
import { chain, createRouter } from 'rouzer'
import { routes } from './routes'
const middlewares = chain().use(ctx => {
// An example middleware. For more info, see https://github.com/alien-rpc/alien-middleware#readme
return {
db: postgres(ctx.env('POSTGRES_URL')),
}
})
export const handler = createRouter({
routes,
middlewares,
debug: process.env.NODE_ENV === 'development',
})({
helloRoute: {
GET(ctx) {
const message = `Hello, ${ctx.path.name}${ctx.query.excited ? '!' : '.'}`
return { message }
},
},
})Router options
export const handler = createRouter({
routes,
middlewares,
basePath: 'api/',
cors: {
allowOrigins: ['example.net', 'https://*.example.com', '*://localhost:3000'],
},
debug: process.env.NODE_ENV === 'development',
})({
helloRoute: {
GET(ctx) {
const message = `Hello, ${ctx.path.name}${ctx.query.excited ? '!' : '.'}`
return { message }
},
},
})basePathis prepended to every route (leading/trailing slashes are trimmed).- CORS preflight (
OPTIONS) is handled automatically for matched routes. cors.allowOriginsrestricts preflight requests to a list of origins (default is to allow any origin).- Wildcards are supported for protocol and subdomain; the protocol is optional and defaults to
https.
- Wildcards are supported for protocol and subdomain; the protocol is optional and defaults to
- If you rely on
CookieorAuthorizationrequest headers, you must setAccess-Control-Allow-Credentialsin your handler.
Client wrapper
import { createClient } from 'rouzer'
import { helloRoute } from './routes'
const client = createClient({ baseURL: '/api/' })
const { message } = await client.json(
helloRoute.GET({ path: { name: 'world' }, query: { excited: true } })
)
// If you want the Response object, use `client.request` instead.
const response = await client.request(
helloRoute.GET({ path: { name: 'world' } })
)
const { message } = await response.json()Add an endpoint
- Declare it in
routes.tswithroute(...)andzod/minischemas. - Implement the handler in your router assembly with
createRouter(…)({ ... }). - Call it from the client with the generated helper via
client.jsonorclient.request.
