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/getting-started/quickstart.md.

Quickstart

Build a small tasks API on Express in a few minutes: a first route, validated routes, a returned error, the API reference, and a test.

1. Create the project

mkdir tasks-api && cd tasks-api
npm init -y
npm pkg set type=module
npm
yarn
pnpm
bun
deno
npm install @dotpress/core @dotpress/adapter-express express zod

Then add the tsconfig.json of Installation.

2. Your first route

src/server.ts
import { createApp, defineRoute } from '@dotpress/core'
import { expressAdapter } from '@dotpress/adapter-express'

defineRoute({
  path: '/hello/:name',
  handler: ({ req }) => ({ message: `Hello, ${req.params.name}!` }),
})

const app = await createApp({ adapter: expressAdapter() })
await app.listen(3000)

Run it, with Node.js 22.18 or later:

node --watch src/server.ts
curl localhost:3000/hello/Ada
# {"message":"Hello, Ada!"}

The handler returns a value: Dotpress sends it as JSON. req.params.name is typed string, from the path.

3. Validated routes

Move the routes to their own file, and add a group of routes for the tasks. The schemas validate each request, and type it:

src/routes.ts
import { 
const createRouteGroup: <const P extends string, const TProvided extends readonly unknown[] = unknown[]>(prefix: P, middlewares?: MiddlewareList<TProvided> | undefined) => RouteGroup<EmptyObject & PathParams<P>, UnionToIntersection<{ [K in keyof TProvided]: unknown extends TProvided[K] ? never : TProvided[K]; }[number]>>

Creates a route group on the global router.

createRouteGroup
,
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
,
const notFoundError: (message?: string, data?: ErrorData) => HttpError
notFoundError
} from '@dotpress/core'
import {
import z
z
} from 'zod'
defineRoute<"/hello/:name", EmptyObject, false, unknown[]>(route: RouteConfig<"/hello/:name", EmptyObject, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<"/hello/:name", EmptyObject, EmptyObject, false, unknown[], unknown>.path: "/hello/:name"
path
: '/hello/:name',
RouteConfig<"/hello/:name", EmptyObject, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {
    name: string;
}, false, unknown>
handler
: ({
req: RouteRequest<EmptyObject, {
    name: string;
}, false>
req
}) => ({
message: string
message
: `Hello, ${
req: RouteRequest<EmptyObject, {
    name: string;
}, false>
req
.
DotpressRequest<{ name: string; }, QueryValues, unknown, UploadedFiles>.params: {
    name: string;
}
params
.
name: string
name
}!` }),
}) const
const Task: z.ZodObject<{
    id: z.ZodUUID;
    title: z.ZodString;
    done: z.ZodBoolean;
}, z.core.$strip>
Task
=
import z
z
.
function object<{
    id: z.ZodUUID;
    title: z.ZodString;
    done: z.ZodBoolean;
}>(shape?: {
    id: z.ZodUUID;
    title: z.ZodString;
    done: z.ZodBoolean;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodUUID;
    title: z.ZodString;
    done: z.ZodBoolean;
}, z.core.$strip>
object
({
id: z.ZodUUID
id
:
import z
z
.
function uuid(params?: string | z.core.$ZodUUIDParams): z.ZodUUID
uuid
(),
title: z.ZodString
title
:
import z
z
.
function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)
string
(),
done: z.ZodBoolean
done
:
import z
z
.
function boolean(params?: string | z.core.$ZodBooleanParams): z.ZodBoolean
boolean
() })
type
type Task = {
    id: string;
    title: string;
    done: boolean;
}
Task
=
import z
z
.
type infer<T> = T extends {
    _zod: {
        output: any;
    };
} ? T["_zod"]["output"] : unknown
export infer
infer
<typeof
const Task: z.ZodObject<{
    id: z.ZodUUID;
    title: z.ZodString;
    done: z.ZodBoolean;
}, z.core.$strip>
Task
>
const
const store: Map<string, {
    id: string;
    title: string;
    done: boolean;
}>
store
= new
var Map: MapConstructor
new <string, {
    id: string;
    title: string;
    done: boolean;
}>(iterable?: Iterable<readonly [string, {
    id: string;
    title: string;
    done: boolean;
}]> | null | undefined) => Map<string, {
    id: string;
    title: string;
    done: boolean;
}> (+3 overloads)
Map
<string,
type Task = {
    id: string;
    title: string;
    done: boolean;
}
Task
>()
const
const tasks: RouteGroup<EmptyObject & {}, unknown>
tasks
=
createRouteGroup<"/tasks", unknown[]>(prefix: "/tasks", middlewares?: readonly Middleware<unknown>[] | undefined): RouteGroup<EmptyObject & {}, unknown>

Creates a nested group, inheriting the prefix and middlewares. Context properties guaranteed by the group middlewares are typed in the handlers of its routes.

createRouteGroup
('/tasks')
const tasks: RouteGroup<EmptyObject & {}, unknown>
tasks
.
RouteGroup<EmptyObject & {}, unknown>.defineRoute<"/", {
    body: z.ZodObject<{
        title: z.ZodString;
    }, z.core.$strip>;
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}, false, unknown[]>(route: RouteConfig<"/", {
    body: z.ZodObject<{
        title: z.ZodString;
    }, z.core.$strip>;
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}, EmptyObject & {}, false, unknown[], unknown>): void
defineRoute
({
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.method?: HttpMethod | undefined

Defaults to get.

method
: 'post',
RouteConfig<"/", { body: ZodObject<{ title: ZodString; }, $strip>; response: ZodObject<{ id: ZodUUID; title: ZodString; done: ZodBoolean; }, $strip>; }, EmptyObject & {}, false, unknown[], unknown>.path: "/"
path
: '/',
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.status?: number | undefined

Success status. Defaults to 200 (204 when nothing is returned).

status
: 201,
RouteConfig<"/", { body: ZodObject<{ title: ZodString; }, $strip>; response: ZodObject<{ id: ZodUUID; title: ZodString; done: ZodBoolean; }, $strip>; }, EmptyObject & {}, false, unknown[], unknown>.schema?: SchemaDefinition<{
    body: z.ZodObject<{
        title: z.ZodString;
    }, z.core.$strip>;
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}> | undefined
schema
: (
z: typeof import("zod")
z
) => ({
body: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>
body
:
z: typeof import("zod")
z
.
function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>
object
({
title: z.ZodString
title
:
z: typeof import("zod")
z
.
function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)
string
().
_ZodString<$ZodStringInternals<string>>.trim(): z.ZodString
trim
().
_ZodString<$ZodStringInternals<string>>.min(minLength: number, params?: string | z.core.$ZodCheckMinLengthParams): z.ZodString
min
(1) }),
response: z.ZodObject<{
    id: z.ZodUUID;
    title: z.ZodString;
    done: z.ZodBoolean;
}, z.core.$strip>
response
:
const Task: z.ZodObject<{
    id: z.ZodUUID;
    title: z.ZodString;
    done: z.ZodBoolean;
}, z.core.$strip>
Task
,
}),
RouteConfig<"/", { body: ZodObject<{ title: ZodString; }, $strip>; response: ZodObject<{ id: ZodUUID; title: ZodString; done: ZodBoolean; }, $strip>; }, EmptyObject & {}, false, unknown[], unknown>.handler: RouteHandler<{
    body: z.ZodObject<{
        title: z.ZodString;
    }, z.core.$strip>;
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}, {}, false, unknown>
handler
: ({
req: RouteRequest<{
    body: z.ZodObject<{
        title: z.ZodString;
    }, z.core.$strip>;
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}, {}, false>
req
}) => {
const
const task: {
    id: `${string}-${string}-${string}-${string}-${string}`;
    title: string;
    done: boolean;
}
task
= {
id: `${string}-${string}-${string}-${string}-${string}`
id
:
var crypto: Crypto
crypto
.
Crypto.randomUUID(): `${string}-${string}-${string}-${string}-${string}`

The randomUUID() method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator. Available only in secure contexts.

MDN Reference

randomUUID
(),
title: string
title
:
req: RouteRequest<{
    body: z.ZodObject<{
        title: z.ZodString;
    }, z.core.$strip>;
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}, {}, false>
req
.
DotpressRequest<{}, QueryValues, { title: string; }, UploadedFiles>.body: {
    title: string;
}
body
.
title: string
title
,
done: boolean
done
: false }
const store: Map<string, {
    id: string;
    title: string;
    done: boolean;
}>
store
.
Map<string, { id: string; title: string; done: boolean; }>.set(key: string, value: {
    id: string;
    title: string;
    done: boolean;
}): Map<string, {
    id: string;
    title: string;
    done: boolean;
}>

Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated.

set
(
const task: {
    id: `${string}-${string}-${string}-${string}-${string}`;
    title: string;
    done: boolean;
}
task
.
id: `${string}-${string}-${string}-${string}-${string}`
id
,
const task: {
    id: `${string}-${string}-${string}-${string}-${string}`;
    title: string;
    done: boolean;
}
task
)
return
const task: {
    id: `${string}-${string}-${string}-${string}-${string}`;
    title: string;
    done: boolean;
}
task
}, })
const tasks: RouteGroup<EmptyObject & {}, unknown>
tasks
.
RouteGroup<EmptyObject & {}, unknown>.defineRoute<"/:id", {
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}, false, unknown[]>(route: RouteConfig<"/:id", {
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}, EmptyObject & {}, false, unknown[], unknown>): void
defineRoute
({
RouteConfig<"/:id", { response: ZodObject<{ id: ZodUUID; title: ZodString; done: ZodBoolean; }, $strip>; }, EmptyObject & {}, false, unknown[], unknown>.path: "/:id"
path
: '/:id',
RouteConfig<"/:id", { response: ZodObject<{ id: ZodUUID; title: ZodString; done: ZodBoolean; }, $strip>; }, EmptyObject & {}, false, unknown[], unknown>.schema?: SchemaDefinition<{
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}> | undefined
schema
: () => ({
response: z.ZodObject<{
    id: z.ZodUUID;
    title: z.ZodString;
    done: z.ZodBoolean;
}, z.core.$strip>
response
:
const Task: z.ZodObject<{
    id: z.ZodUUID;
    title: z.ZodString;
    done: z.ZodBoolean;
}, z.core.$strip>
Task
}),
RouteConfig<"/:id", { response: ZodObject<{ id: ZodUUID; title: ZodString; done: ZodBoolean; }, $strip>; }, EmptyObject & {}, false, unknown[], unknown>.handler: RouteHandler<{
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}, {
    id: string;
}, false, unknown>
handler
: ({
req: RouteRequest<{
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}, {
    id: string;
}, false>
req
}) =>
const store: Map<string, {
    id: string;
    title: string;
    done: boolean;
}>
store
.
Map<string, { id: string; title: string; done: boolean; }>.get(key: string): {
    id: string;
    title: string;
    done: boolean;
} | undefined

Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.

@returnsReturns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
get
(
req: RouteRequest<{
    response: z.ZodObject<{
        id: z.ZodUUID;
        title: z.ZodString;
        done: z.ZodBoolean;
    }, z.core.$strip>;
}, {
    id: string;
}, false>
req
.
DotpressRequest<{ id: string; }, QueryValues, unknown, UploadedFiles>.params: {
    id: string;
}
params
.
id: string
id
) ??
function notFoundError(message?: string, data?: ErrorData): HttpError
notFoundError
('Task not found'),
})

Hover req.body and req.params. Then import the routes in src/server.ts, before creating the app:

src/server.ts
import { createApp } from '@dotpress/core'
import { expressAdapter } from '@dotpress/adapter-express'
import './routes.ts'

const app = await createApp({ adapter: expressAdapter() })
await app.listen(3000)

An invalid body never reaches the handler:

curl -X POST localhost:3000/tasks -H 'content-type: application/json' -d '{"title":""}'
{
  "status": 400,
  "error": "VALIDATION_FAILED",
  "message": "Validation failed",
  "data": {
    "issues": [
      {
        "source": "body",
        "path": ["title"],
        "code": "too_small",
        "message": "Too small: expected string to have >=1 characters"
      }
    ]
  }
}

And an unknown task gets the error returned by the handler:

curl localhost:3000/tasks/unknown
# {"status":404,"error":"NOT_FOUND","message":"Task not found"}

4. The API reference

npm
yarn
pnpm
bun
deno
npm install @dotpress/openapi

The OpenAPI plugin builds the document from your routes and schemas. Create the app in its own file, so that the tests can create it too:

src/app.ts
import { createApp, type HttpAdapter } from '@dotpress/core'
import { openapiPlugin } from '@dotpress/openapi'
import './routes.ts'

export const createTasksApp = <TAdapter extends HttpAdapter>(adapter: TAdapter) =>
  createApp({
    adapter,
    plugins: [openapiPlugin({ info: { title: 'Tasks API', version: '1.0.0' } })],
  })
src/server.ts
import { expressAdapter } from '@dotpress/adapter-express'
import { createTasksApp } from './app.ts'

const app = await createTasksApp(expressAdapter())
await app.listen(3000)

Open localhost:3000/docs: every route is documented, with its parameters, body and responses. The document itself is served at /openapi.json.

5. A test

npm
yarn
pnpm
bun
deno
npm install -D vitest @dotpress/testing

The memory adapter runs the requests in memory, without opening a port: the test uses the app as configured in production.

test/tasks.test.ts
import { memoryAdapter } from '@dotpress/testing'
import { expect, it } from 'vitest'
import { createTasksApp } from '../src/app.ts'

it('creates a task', async () => {
  const adapter = memoryAdapter()
  await createTasksApp(adapter)

  const res = await adapter.fetch(
    new Request('http://localhost/tasks', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ title: 'Write a test' }),
    })
  )

  expect(res.status).toBe(201)
  expect(await res.json()).toMatchObject({ title: 'Write a test', done: false })
})
npx vitest run

Next steps