# Express integration

> Use @hatched/sdk-js from an Express app — handlers, raw-body webhook middleware, and error mapping.

Source: https://docs.hatched.live/docs/guides/express-integration

Express is straightforward. The SDK is server-only, so everything works
out of the box as long as you keep your API key in an env variable and
preserve raw bodies for webhooks.

## Install

```bash
pnpm add @hatched/sdk-js express
```

## Shared client

```ts
// src/hatched.ts
import { HatchedClient } from '@hatched/sdk-js';

export const hatched = new HatchedClient({
  apiKey: process.env.HATCHED_API_KEY!,
});
```

## Route handler example

```ts
// src/routes/events.ts
import { Router } from 'express';
import { ValidationError } from '@hatched/sdk-js';
import { hatched } from '../hatched';

export const events = Router();

events.post('/lesson-completed', async (req, res, next) => {
  try {
    const { userId, lessonId, score } = req.body;
    const effects = await hatched.events.send({
      eventId: `lesson_${lessonId}_${userId}`,
      userId,
      type: 'lesson_completed',
      properties: { lessonId, score },
    });
    res.json(effects);
  } catch (err) {
    if (err instanceof ValidationError) {
      return res.status(422).json({ error: err.details });
    }
    next(err);
  }
});
```

## Webhook endpoint — raw body first

The default `express.json()` middleware parses and throws away the raw
body, which breaks signature verification. Mount a raw-body parser on
**just** the webhook path:

```ts
// src/index.ts
import express from 'express';
import { WebhooksResource } from '@hatched/sdk-js';

const app = express();

app.post(
  '/webhooks/hatched',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.header('hatched-signature') ?? '';
    const valid = WebhooksResource.verifySignature(
      req.body, // Buffer, from express.raw
      signature,
      process.env.HATCHED_WEBHOOK_SECRET!,
    );
    if (!valid) return res.status(400).send('invalid signature');

    const event = JSON.parse(req.body.toString('utf8'));
    // enqueue, etc.
    res.status(202).end();
  },
);

// JSON parser for everything else
app.use(express.json());
// ... your other routes
```

## Centralised error mapping

```ts
import { HatchedError, RateLimitError } from '@hatched/sdk-js';

app.use((err, _req, res, _next) => {
  if (err instanceof RateLimitError) {
    res.set('Retry-After', String(err.retryAfter));
    return res.status(429).json({ error: 'rate_limited' });
  }
  if (err instanceof HatchedError) {
    return res.status(err.statusCode).json({
      error: { code: err.code, message: err.message, requestId: err.requestId },
    });
  }
  console.error(err);
  res.status(500).json({ error: 'internal_error' });
});
```

## Graceful shutdown

If you're running long polls (`operations.wait`) during shutdown, pass an
`AbortSignal` so SIGTERM can cancel them cleanly:

```ts
const controller = new AbortController();
process.on('SIGTERM', () => controller.abort());

await hatched.operations.wait(op.operationId, { signal: controller.signal });
```
