MainSDK
Search⌘K
Docs

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.

Planned for 0.5

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
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

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.

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

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet