Skip to main content
npayload is launching soon.
npayloadDocs
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 --init

Create 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:3000

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

Test 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

Was this page helpful?

On this page