> 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.

# Express

A small tasks API on [Express](https://expressjs.com):

- typed routes in a group (`/tasks`), with parameters, query and body validated by Zod schemas;
- errors returned by the handlers (`notFoundError()`, `conflictError()`), documented in `meta.responses`;
- an OpenAPI 3.1 document generated from the routes (`/openapi.json`), and its Scalar API reference (`/docs`);
- tests of the app in memory with `@dotpress/testing`, and on the real Express server.

Source: [`examples/express`](https://github.com/dotpressjs/dotpress/tree/main/examples/express).

## Run it

```sh
npx degit dotpressjs/dotpress/examples/express my-api
cd my-api
npm install
npm run dev
```

## The routes

```ts file="<root>/../examples/express/src/routes.ts"
import {
  conflictError,
  createRouter,
  empty,
  notFoundError,
} from '@dotpress/core'
import { z } from 'zod'
import type { TaskStore } from './tasks.ts'

// A schema with an identifier is declared once in the OpenAPI document
// (components/schemas), and referenced by the routes.
const Task = z
  .object({
    id: z.uuid(),
    title: z.string(),
    done: z.boolean(),
    createdAt: z.iso.datetime(),
  })
  .meta({ id: 'Task', description: 'A task to do' })

const Title = z.string().trim().min(1).max(200)

const TASK_NOT_FOUND = 'Task not found'
const DUPLICATE_TITLE = 'A task with this title already exists'

/**
 * Routes of the tasks. The store is a parameter: tests and environments
 * give their own, without a dependency injection container.
 */
export const tasksRouter = (store: TaskStore) => {
  const router = createRouter()
  const tasks = router.createGroup('/tasks')

  tasks.defineRoute({
    path: '/',
    schema: (z) => ({
      query: z.object({ done: z.stringbool().optional() }),
      response: z.array(Task),
    }),
    meta: { summary: 'List the tasks', tags: ['tasks'] },
    // req.query: { done?: boolean }
    handler: ({ req }) => store.list({ done: req.query.done }),
  })

  tasks.defineRoute({
    method: 'post',
    path: '/',
    status: 201,
    schema: (z) => ({
      body: z.object({ title: Title }),
      response: Task,
    }),
    meta: {
      summary: 'Create a task',
      tags: ['tasks'],
      responses: { 409: DUPLICATE_TITLE },
    },
    handler: async ({ req }) => {
      // Errors are returned, not thrown: they are part of the handler type.
      if (await store.findByTitle(req.body.title)) {
        return conflictError(DUPLICATE_TITLE)
      }
      return store.create(req.body.title)
    },
  })

  tasks.defineRoute({
    path: '/:id',
    schema: () => ({ response: Task }),
    meta: {
      summary: 'Get a task',
      tags: ['tasks'],
      responses: { 404: TASK_NOT_FOUND },
    },
    // req.params: { id: string }, from the path
    handler: async ({ req }) =>
      (await store.get(req.params.id)) ?? notFoundError(TASK_NOT_FOUND),
  })

  tasks.defineRoute({
    method: 'patch',
    path: '/:id',
    schema: (z) => ({
      body: z.object({ title: Title.optional(), done: z.boolean().optional() }),
      response: Task,
    }),
    meta: {
      summary: 'Update a task',
      tags: ['tasks'],
      responses: { 404: TASK_NOT_FOUND, 409: DUPLICATE_TITLE },
    },
    handler: async ({ req }) => {
      const { title } = req.body
      if (title !== undefined) {
        const existing = await store.findByTitle(title)
        if (existing && existing.id !== req.params.id) {
          return conflictError(DUPLICATE_TITLE)
        }
      }
      return (
        (await store.update(req.params.id, req.body)) ??
        notFoundError(TASK_NOT_FOUND)
      )
    },
  })

  tasks.defineRoute({
    method: 'delete',
    path: '/:id',
    status: 204,
    meta: {
      summary: 'Delete a task',
      tags: ['tasks'],
      responses: { 404: TASK_NOT_FOUND },
    },
    handler: async ({ req }) =>
      (await store.remove(req.params.id))
        ? empty()
        : notFoundError(TASK_NOT_FOUND),
  })

  return router
}

```

## The app

The app takes its adapter as a parameter: Express in `server.ts`, the memory adapter of `@dotpress/testing` in the tests.

```ts file="<root>/../examples/express/src/app.ts"
import { createApp, type HttpAdapter, type Logger } from '@dotpress/core'
import { openapiPlugin } from '@dotpress/openapi'
import { tasksRouter } from './routes.ts'
import { createTaskStore, type TaskStore } from './tasks.ts'

export interface TasksAppOptions<TAdapter extends HttpAdapter> {
  adapter: TAdapter
  store?: TaskStore
  logger?: Logger
}

/**
 * Creates the app on any adapter: Express in `server.ts`, the memory
 * adapter of `@dotpress/testing` in the tests.
 */
export const createTasksApp = <TAdapter extends HttpAdapter>({
  adapter,
  store = createTaskStore(),
  logger,
}: TasksAppOptions<TAdapter>) =>
  createApp({
    adapter,
    ...(logger && { logger }),
    routers: [tasksRouter(store)],
    plugins: [
      // GET /openapi.json: the OpenAPI document, GET /docs: the Scalar page
      openapiPlugin({
        info: { title: 'Tasks API', version: '1.0.0' },
        tags: [{ name: 'tasks', description: 'Tasks to do' }],
      }),
    ],
  })

```

## The server

```ts file="<root>/../examples/express/src/server.ts"
import { expressAdapter } from '@dotpress/adapter-express'
import { createTasksApp } from './app.ts'

const port = Number(process.env.PORT ?? 3000)

const app = await createTasksApp({ adapter: expressAdapter() })
await app.listen(port)
console.log(`Tasks API: http://localhost:${port}/tasks`)
console.log(`API reference: http://localhost:${port}/docs`)

// Waits for pending requests, then runs the onClose hooks of the plugins.
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
  process.once(signal, () => void app.close())
}

```

## Learn more

- [The Express adapter](https://dotpress.dev/adapters/express.md)
- [Validation](https://dotpress.dev/routing/validation.md)
- [OpenAPI](https://dotpress.dev/services/openapi.md)
