Guides
Express integration
Use @hatched/sdk-js from an Express app — handlers, raw-body webhook middleware, and error mapping.
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
pnpm add @hatched/sdk-js expressShared client
// src/hatched.ts
import { HatchedClient } from '@hatched/sdk-js';
export const hatched = new HatchedClient({
apiKey: process.env.HATCHED_API_KEY!,
});Route handler example
// 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:
// 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 routesCentralised error mapping
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:
const controller = new AbortController();
process.on('SIGTERM', () => controller.abort());
await hatched.operations.wait(op.operationId, { signal: controller.signal });