---
title: "Quick start for Express"
description: "Add MainSDK to an Express app with one middleware and one error handler, guard a route with auth.require, run it in local mode and pass mainsdk check."
section: "getting-started"
status: "Planned for 0.5"
spec: "public-beta"
canonical: "https://mainsdk.dev/docs/getting-started/quickstart-express"
updated: "2026-10-28"
---

# Quick start for Express

> Add MainSDK to an Express app with one middleware and one error handler, guard a route with auth.require, run it in local mode and pass mainsdk check.

Status: Planned for 0.5 · Canonical: https://mainsdk.dev/docs/getting-started/quickstart-express · Updated: 2026-10-28

You will add MainSDK to an Express 5 app on Node.js 22 or later, guard one route and run it with no account or key. It takes about 10 minutes.

## 1. Initialise

```bash
npx mainsdk init
```

## 2. Write the route

```ts title="src/routes.ts"
import { auth } from 'mainsdk'

// The route's logic is a plain Request in, Response out, so Express and a test call it the same way.
export async function me(req: Request): Promise<Response> {
  const session = await auth.require(req) // 401 with a fix when nobody is signed in
  return Response.json({ user: session.user })
}

// Express speaks Node requests, so hand the route a standard Request with the same headers.
export function toRequest(req: {
  headers: Record<string, string | string[] | undefined>
  originalUrl: string
}): Request {
  const headers = new Headers()
  for (const [name, value] of Object.entries(req.headers)) {
    if (typeof value === 'string') headers.set(name, value)
  }
  return new Request(`http://${req.headers.host ?? 'localhost'}${req.originalUrl}`, { headers })
}
```

## 3. Mount MainSDK

`mainsdk()` goes first and `mainsdk.errors()` goes last, so a thrown error becomes a problem response.

```ts title="src/app.ts"
import express from 'express'
import { mainsdk } from 'mainsdk/express'
import { me, toRequest } from './routes.ts'

export const app = express()
app.use(mainsdk())
app.get('/me', async (req, res) => {
  const out = await me(toRequest(req))
  res
    .status(out.status)
    .type('application/json')
    .send(await out.text())
})
app.use(mainsdk.errors()) // last: turns an error into a problem response
```

```ts title="src/server.ts"
import { app } from './app.ts'

app.listen(Number(process.env.PORT ?? 3000))
```

## 4. Run it in local mode

In local mode the bearer token `dev_ada` stands for a test person called `ada`. It works only on loopback, under `mainsdk dev`.

```bash
npx mainsdk dev -- node --experimental-strip-types src/server.ts
curl -i localhost:3000/me
curl -i -H "Authorization: Bearer dev_ada" localhost:3000/me
```

The first call answers `401` with a problem body that carries `MAIN-AUTH-0001` and a fix. The second answers `200` with the user.

## Check it

```bash
npx mainsdk check
```

The check passes when it prints `Passed`. It fails on a key in code, an undeclared network call or an unguarded action, and names the fix for each. Run it in CI too.

## What the sample app is

The finished app is in the repository under `apps/examples/express`. CI installs it, starts it in local mode, requests the signed-in route and runs `mainsdk check`, so these steps cannot drift from the code. MainSDK is pre-release and no package is published yet: until the first beta, run the sample from a checkout.
