Framework Quickstarts
Express quickstart
Build a REST API with Express that publishes events and receives webhooks
Build a production-ready Express API with TypeScript that publishes structured events to npayload and receives webhooks with proper error handling.
Prerequisites
- Node.js 18+
- An npayload account with machine credentials (get them here)
Set up the project
mkdir npayload-express-api && cd npayload-express-api
npm init -y
npm install @npayload/node express dotenv
npm install -D typescript @types/express @types/node tsx
npx tsc --initCreate a .env file:
NPAYLOAD_CLIENT_ID=oac_your_client_id
NPAYLOAD_HMAC_SECRET=your_hmac_secret
WEBHOOK_SECRET=your_webhook_secret
PORT=3000
BASE_URL=http://localhost:3000Create the npayload client
// src/npayload.ts
import { NPayloadAuth, NPayloadClient } from '@npayload/node';
const auth = new NPayloadAuth({
clientId: process.env.NPAYLOAD_CLIENT_ID!,
hmacSecret: process.env.NPAYLOAD_HMAC_SECRET!,
});
export const npayload = new NPayloadClient({
auth,
environment: 'development',
});Set up channels and subscriptions
// src/setup.ts
import { npayload } from './npayload';
export async function setup() {
await npayload.channels.create({
name: 'user-events',
description: 'User lifecycle events',
privacy: 'standard',
});
await npayload.subscriptions.create({
channel: 'user-events',
name: 'event-processor',
type: 'webhook',
endpoint: {
url: `${process.env.BASE_URL}/webhooks/user-events`,
method: 'POST',
},
});
console.log('Channel and subscription created.');
}Build the Express server
// src/server.ts
import 'dotenv/config';
import express, { Request, Response, NextFunction } from 'express';
import { npayload } from './npayload';
import { setup } from './setup';
const app = express();
app.use(express.json());
// Error handling middleware
function asyncHandler(
fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
) {
return (req: Request, res: Response, next: NextFunction) => {
fn(req, res, next).catch(next);
};
}
// Publish structured events with routing keys
app.post(
'/users/:userId/events',
asyncHandler(async (req, res) => {
const { userId } = req.params;
const { event, data } = req.body;
const message = await npayload.messages.publish({
channel: 'user-events',
routingKey: `user.${event}`,
payload: {
userId,
event,
data,
timestamp: new Date().toISOString(),
},
});
res.status(201).json({
messageId: message.gid,
channel: 'user-events',
routingKey: `user.${event}`,
});
})
);
// Webhook receiver with signature verification
app.post(
'/webhooks/user-events',
asyncHandler(async (req, res) => {
const signature = req.headers['x-npayload-signature'] as string;
const isValid = npayload.webhooks.verify(
req.body,
signature,
process.env.WEBHOOK_SECRET!
);
if (!isValid) {
res.status(401).json({ error: 'Invalid webhook signature' });
return;
}
const { payload } = req.body;
console.log(`[${payload.event}] User ${payload.userId}:`, payload.data);
res.status(200).json({ received: true });
})
);
// Global error handler
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error('Unhandled error:', err.message);
res.status(500).json({ error: 'Internal server error' });
});
const PORT = process.env.PORT || 3000;
setup()
.then(() => {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
})
.catch(console.error);Run it
npx tsx src/server.tsTest publishing a user event:
curl -X POST http://localhost:3000/users/usr_42/events \
-H "Content-Type: application/json" \
-d '{"event": "signup", "data": {"plan": "pro", "source": "landing-page"}}'Next steps
- Hono quickstart for an edge-first alternative
- Node.js SDK for the full API reference
- Webhooks guide for retry policies and failure handling
Was this page helpful?