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/introduction.md.

Introduction

Dotpress is a lightweight TypeScript framework for backend APIs. You declare each route with defineRoute(): a path, Zod schemas and a handler. Dotpress validates the request, types it from your schemas and from the path, and runs your app on the HTTP server you choose.

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 notFoundError: (message?: string, data?: ErrorData) => HttpError
notFoundError
} from '@dotpress/core'
declare const
const members: {
    find(orgId: string, id: string): Promise<{
        id: string;
        name: string;
    } | undefined>;
}
members
: {
function find(orgId: string, id: string): Promise<{
    id: string;
    name: string;
} | undefined>
find
(
orgId: string
orgId
: string,
id: string
id
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<{
id: string
id
: string;
name: string
name
: string } | undefined>
} // Every route of the group starts with /orgs/:orgId/members const
const orgMembers: RouteGroup<EmptyObject & {
    orgId: string;
}, unknown>
orgMembers
=
createRouteGroup<"/orgs/:orgId/members", unknown[]>(prefix: "/orgs/:orgId/members", middlewares?: readonly Middleware<unknown>[] | undefined): RouteGroup<EmptyObject & {
    orgId: string;
}, 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
('/orgs/:orgId/members')
const orgMembers: RouteGroup<EmptyObject & {
    orgId: string;
}, unknown>
orgMembers
.
RouteGroup<EmptyObject & { orgId: string; }, unknown>.defineRoute<"/:memberId", EmptyObject, false, unknown[]>(route: RouteConfig<"/:memberId", EmptyObject, EmptyObject & {
    orgId: string;
}, false, unknown[], unknown>): void
defineRoute
({
RouteConfig<"/:memberId", EmptyObject, EmptyObject & { orgId: string; }, false, unknown[], unknown>.path: "/:memberId"
path
: '/:memberId',
RouteConfig<"/:memberId", EmptyObject, EmptyObject & { orgId: string; }, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {
    orgId: string;
    memberId: string;
}, false, unknown>
handler
: async ({
req: RouteRequest<EmptyObject, {
    orgId: string;
    memberId: string;
}, false>
req
}) => {
const
const member: {
    id: string;
    name: string;
} | undefined
member
= await
const members: {
    find(orgId: string, id: string): Promise<{
        id: string;
        name: string;
    } | undefined>;
}
members
.
function find(orgId: string, id: string): Promise<{
    id: string;
    name: string;
} | undefined>
find
(
req: RouteRequest<EmptyObject, {
    orgId: string;
    memberId: string;
}, false>
req
.
DotpressRequest<{ orgId: string; memberId: string; }, QueryValues, unknown, UploadedFiles>.params: {
    orgId: string;
    memberId: string;
}
params
.
orgId: string
orgId
,
req: RouteRequest<EmptyObject, {
    orgId: string;
    memberId: string;
}, false>
req
.
DotpressRequest<{ orgId: string; memberId: string; }, QueryValues, unknown, UploadedFiles>.params: {
    orgId: string;
    memberId: string;
}
params
.
memberId: string
memberId
)
return
const member: {
    id: string;
    name: string;
} | undefined
member
??
function notFoundError(message?: string, data?: ErrorData): HttpError
notFoundError
('Member not found')
}, })

Hover req.params: orgId comes from the group, memberId from the route. No annotation, no decorator, no class.

Why Dotpress

Building an API in TypeScript usually means picking one of two compromises:

  • A minimal HTTP framework (Express, Fastify, Hono): quick to start, but the validation, the request types, the error format and the documentation are yours to write, and to keep in sync by hand. Middleware chains grow, and nothing tells the handler what they did.
  • A full framework with decorators, classes and a dependency injection container: structured, but verbose, and tied to its own way of doing everything.

With a minimal framework, the application also ends up coupled to it. Handlers take its request and response objects, middlewares follow its conventions, and errors go through its error handlers. Each framework does these differently, so moving to another one, or to another way of running the app, means rewriting the HTTP layer.

Dotpress sits in between: the structure of a framework, with the syntax of plain functions and objects. It standardizes the HTTP layer and abstracts it from the server:

  • one way to write your API: the same routes, middlewares, errors and responses, whatever the HTTP framework underneath;
  • the server is a choice you can change: Express, Fastify or Hono, picked by an adapter, in one line;
  • so is the way you run it: a long-running Node.js instance or AWS Lambda for the API, and workers for the events, with the same code.

What you get

  • Routes as declarations. defineRoute() and createRouteGroup() take plain objects. Groups share a path prefix and middlewares, and nest.
  • Validation and types from your schemas. Zod schemas validate the path parameters, the query, the body, the uploaded files and the response. The handler receives the parsed values, typed. Path parameters are typed from the path itself.
  • Errors are values. A handler returns notFoundError() like any other value. Every error, from a failed validation to an unexpected exception, uses the same JSON format.
  • Typed middlewares. A middleware can guarantee a value to the handlers that use it, such as the authenticated user, and TypeScript knows it.
  • Any HTTP server. The core only relies on Web standards. The same app runs on Express, Fastify or Hono, on Node.js or AWS Lambda, with the same routing rules.
  • Tested in memory. The memory adapter runs requests without opening a port.
  • Packages when you need them. Authentication, OpenAPI documents, emails, events, cache and testing helpers, each in its own package, each optional.

What Dotpress is not

  • Not a full-stack framework. No views, no ORM: use the database library you prefer.
  • No dependency injection container. Services are plain functions and objects, given to the app or exposed through the request context (see Project structure).
  • Not on edge runtimes in 2.0. The core is portable, but Dotpress 2.0 supports Node.js and AWS Lambda (see the design decision).

Packages

PackagePurpose
@dotpress/coreRoutes, validation, request pipeline, responses, errors, plugins
@dotpress/adapter-expressExpress 4 and 5
@dotpress/adapter-fastifyFastify 5, on Node.js and AWS Lambda
@dotpress/adapter-honoHono 4, on Node.js and AWS Lambda
@dotpress/authJWT authentication: login, refresh token rotation, cookie sessions, typed ctx.user
@dotpress/openapiOpenAPI 3.1 documents from your routes, and a Scalar API reference
@dotpress/mailerTyped, multilingual email templates (MJML or HTML)
@dotpress/eventsTyped events and listeners, run by workers
@dotpress/cacheRequest memoization with batching, and a resilient cache
@dotpress/testingMemory adapter and assertions for tests
Alpha

Dotpress 2 is in alpha (2.0.0-alpha): the API may still change before 2.0.0.

Coming from v1

Dotpress 2 is a rewrite. dotpress is replaced by @dotpress/core and an adapter, and most options changed name. See Migrate from v1.