Migrating from Express
Ce contenu n’est pas encore disponible dans votre langue.
Lacis covers most of what teams reach for express + a handful of middleware packages to do —
routing, body parsing, cookies, CORS, rate limiting, validation — with zero runtime
dependencies and file-based routing. This guide maps Express concepts to their Lacis
equivalents.
At a glance
Section titled “At a glance”| Express | Lacis |
|---|---|
app.get('/users', h) | file routes/users/index.ts exporting GET |
express.Router() | directory nesting under routes/ |
req.params / req.query | req.params / req.query (typed via defineHandler) |
express.json() + req.body | await req.json() (built in) |
express.urlencoded() + req.body | await req.form() (built in) |
cookie-parser + req.cookies | req.cookies.get() / res.cookies.set() (built in) |
res.json / res.send / res.redirect | same |
res.set(name, val) | res.setHeader(name, val) |
app.use(mw) (global) | middleware.beforeRequest or +middleware.global.ts |
app.post('/x', auth, h) (per-route) | use: [auth] in defineHandler |
next() / next(err) | return (or return false to stop); throw to error |
app.use(errorHandler) | hooks.onError + createNotFoundError(...) / sendError helpers |
cors package | built-in cors config |
express-rate-limit | built-in createRateLimit |
joi / zod middleware | defineHandler({ params, query, body }) |
app.listen(3000) | createServer(routesDir, { port: 3000 }) |
Routing
Section titled “Routing”Express registers routes imperatively; Lacis derives them from the filesystem.
const app = express()
app.get('/users', getUsers)app.post('/users', createUser)app.get('/users/:id', getUser)// routes/users/index.tsexport const GET = getUsersexport const POST = createUser
// routes/users/[id]/index.tsexport const GET = getUserSee Routing for the full file conventions.
Request body
Section titled “Request body”Express needs express.json() / express.urlencoded() registered before req.body is
populated. Lacis parses on demand — no setup.
app.use(express.json())app.use(express.urlencoded({ extended: true }))
app.post('/users', (req, res) => { const { name } = req.body res.status(201).json({ name })})// routes/users/index.tsexport async function POST(req, res) { const { name } = await req.json() // JSON bodies // const form = await req.form() // multipart or urlencoded res.status(201).json({ name })}Middleware
Section titled “Middleware”Global middleware maps to server config or a +middleware.global.ts file. Per-route
middleware — including per-method scoping that Express expresses positionally — maps to
use: in defineHandler.
// globalapp.use(logger)
// per-route, runs only for this handlerapp.post('/users', rateLimit, auth, createUser)// global — server.tscreateServer('./routes', { middleware: { beforeRequest: logger } })
// per-route/per-method — routes/users/index.tsexport const POST = defineHandler({ use: [rateLimit, auth], handler: createUser,})Validation
Section titled “Validation”Express has no built-in validation; you reach for joi, express-validator, or a zod
middleware. Lacis builds it into defineHandler via Standard Schema,
and the same schema feeds your OpenAPI spec.
import { defineHandler } from 'lacis'import { z } from 'zod'
export const POST = defineHandler({ body: z.object({ name: z.string(), email: z.string().email() }), handler: async (req, res) => { const { name, email } = req.body // typed & validated; 400 returned automatically on failure res.status(201).json({ name, email }) },})Error handling
Section titled “Error handling”Express uses a 4-argument error middleware. Lacis catches thrown errors automatically and
routes them to the onError hook; typed HTTP error helpers replace manual status codes.
app.use((err, req, res, next) => { res.status(err.status ?? 500).json({ error: err.message })})import { createServer, createNotFoundError } from 'lacis'
// throw anywhere in a handler — Lacis sends the right status automatically:throw createNotFoundError('User not found')
// central hook — server.tscreateServer('./routes', { middleware: { // Keep onError log-only: Lacis sends the correct status for a thrown error // automatically once this returns. Sending a response here would override it // and downgrade every thrown 401/404/409 to whatever you send. onError: async (req, res, ctx) => { console.error(ctx.error) }, },})See Error Handling for the full set of error constructors.
CORS & rate limiting
Section titled “CORS & rate limiting”Replace the cors and express-rate-limit packages with built-in config:
import { createServer, createRateLimit } from 'lacis'
createServer('./routes', { cors: { origin: 'https://myapp.com', credentials: true }, middleware: { beforeRequest: createRateLimit({ windowMs: 60_000, max: 100 }), },})Starting the server
Section titled “Starting the server”app.listen(3000, () => console.log('listening'))import { nodeAdapter } from 'lacis/adapters'
const createServer = nodeAdapter.createHandler('./routes')createServer({ port: 3000 })The same routes run on Bun, Vercel, Netlify, and Cloudflare by swapping the adapter — see Deployment.
What Lacis intentionally does not have
Section titled “What Lacis intentionally does not have”- A pluggable
app.use()stack of arbitrary connect middleware. Express middleware that depends on the connect signature ((req, res, next)) won’t drop in unchanged — port the logic to abeforeRequest/use:function. Most are a few lines. - A
view/template engine integration. Render withres.html(...)or return JSON for an API-first app.