For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt, and this page is available as Markdown at https://dotpress.dev/adapters/express.md.

Express

@dotpress/adapter-express runs your app on Express 4 or 5.

npm
yarn
pnpm
bun
deno
npm install @dotpress/core @dotpress/adapter-express express zod
import { 
const createApp: <TAdapter extends HttpAdapter<unknown>>(options: AppOptions<TAdapter>) => Promise<App<NativeOf<TAdapter>>>

Creates a Dotpress application and mounts its routes on the adapter.

createApp
,
const defineRoute: <const P extends string, S extends RouteSchema = EmptyObject, TRawBody extends boolean = false, const TProvided extends readonly unknown[] = unknown[]>(route: RouteConfig<P, S, EmptyObject, TRawBody, TProvided, unknown>) => void

Declares a route on the global router.

defineRoute
} from '@dotpress/core'
import {
function expressAdapter(options?: ExpressAdapterOptions<e.Express>): ExpressAdapter<e.Express> (+1 overload)

Creates the Express adapter (Express 4 or 5).

@exampleconst app = await createApp({ adapter: expressAdapter({ beforeRoutes: (app) => app.use(helmet()), }), }) await app.listen(3000)
expressAdapter
} from '@dotpress/adapter-express'
defineRoute<"/hello", EmptyObject, false, unknown[]>(route: RouteConfig<"/hello", EmptyObject, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<"/hello", EmptyObject, EmptyObject, false, unknown[], unknown>.path: "/hello"
path
: '/hello',
RouteConfig<"/hello", EmptyObject, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {}, false, unknown>
handler
: () => ({
message: string
message
: 'Hello' }) })
const
const app: App<e.Express>
app
= await
createApp<ExpressAdapter<e.Express>>(options: AppOptions<ExpressAdapter<e.Express>>): Promise<App<e.Express>>

Creates a Dotpress application and mounts its routes on the adapter.

createApp
({
AppOptions<ExpressAdapter<e.Express>>.adapter: ExpressAdapter<e.Express>

HTTP adapter (Express, Hono, Fastify…).

adapter
:
function expressAdapter(options?: ExpressAdapterOptions<e.Express>): ExpressAdapter<e.Express> (+1 overload)

Creates the Express adapter (Express 4 or 5).

@exampleconst app = await createApp({ adapter: expressAdapter({ beforeRoutes: (app) => app.use(helmet()), }), }) await app.listen(3000)
expressAdapter
() })
await
const app: App<e.Express>
app
.
App<e.Express>.listen(port: number, host?: string): Promise<void>

Starts the server. Not available on serverless adapters.

listen
(3000)

Options

OptionDescription
appAn existing Express application (Express 4 or 5). By default, a new express() application, with X-Powered-By and ETag disabled.
beforeRoutes(app)Registers native middlewares before the Dotpress routes.
afterRoutes(app)Registers native middlewares and routes after the Dotpress routes, before the not-found handler.
bodyLimitMaximum request body size, in bytes. 1 MiB by default.

Express middlewares

Middlewares of the Express ecosystem go in beforeRoutes:

import compression from 'compression'
import helmet from 'helmet'

const app = await createApp({
  adapter: expressAdapter({
    beforeRoutes: (app) => {
      app.use(helmet())
      app.use(compression())
    },
  }),
})

They run before Dotpress, for every request. Keep the logic of your API in Dotpress middlewares: they see the matched route and the typed context, run on every adapter, and are tested in memory.

An existing application

Pass your application with app: its settings, middlewares and routes stay, and the Dotpress routes are added to it. A project can move its routes to Dotpress one at a time.

import express from 'express'

const legacy = express()
legacy.set('trust proxy', 1) // behind one proxy: ctx.req.ip is the client address
legacy.get('/legacy/status', (req, res) => res.json({ ok: true }))

const app = await createApp({ adapter: expressAdapter({ app: legacy }) })

Behavior

  • Routes are matched by Dotpress, with the same rules on every adapter. Express settings such as case sensitive routing or strict routing do not apply to them.
  • Bodies are read by the adapter: JSON, forms and text are parsed, and uploads without Multer. A body already parsed by a native middleware (express.json(), multer…) is used as is. Routes declared with rawBody: true need the exact bytes: do not register a native body parser for them.
  • Errors passed to next(error) by native middlewares use the Dotpress error format: a 4xx status is kept, anything else is a 500 reported to onError.
  • ctx.req.ip honors the trust proxy setting of Express.
  • ctx.raw is the Express request.
  • close() waits for pending requests and closes the responses being streamed.
  • adapter.server() returns the Node.js HTTP server started by listen().

Your own HTTP server

To serve HTTPS from Node.js, or share a server, mount the Express application yourself:

import https from 'node:https'

const app = await createApp({ adapter: expressAdapter() })
https.createServer(tlsOptions, app.native()).listen(443)

AWS Lambda

The Express adapter has no Lambda entry. Your routes do not depend on the adapter: use the Hono or Fastify adapter on AWS Lambda, and keep Express for your servers if you like.

Example

The Express example is a tasks API with route groups, validation, returned errors, OpenAPI, and tests in memory and over HTTP.

Coming from v1

useBeforeRoutes and useAfterRoutes of createApp() become the beforeRoutes and afterRoutes options of the adapter. createApp() no longer returns the Express application: use app.native().