Implement the contract: let customers register webhooks and emit events. By the end, you'll have POST /webhooks, POST /events, and proper data validation.
POST /webhooks accepts URL, secret, event_types; stores in DB; returns webhook ID.GET /webhooks/:id retrieves webhook metadata.POST /events accepts event type and payload; stores immediately in DB.Create src/db/migrations.ts:
import { Pool } from 'pg';
export async function runMigrations(pool: Pool) {
const client = await pool.connect();
try {
await client.query(`
CREATE TABLE IF NOT EXISTS webhooks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
url TEXT NOT NULL,
secret TEXT NOT NULL,
event_types TEXT[] NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS deliveries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
webhook_id UUID NOT NULL REFERENCES webhooks(id),
event_id UUID NOT NULL REFERENCES events(id),
status TEXT NOT NULL DEFAULT 'pending',
attempts INT DEFAULT 0,
last_error TEXT,
next_retry_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
`);
console.log('Migrations completed successfully');
} finally {
client.release();
}
}
src/db/migrations.ts.Create src/db/pool.ts:
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgres://postgres:password@localhost:5432/webhook_service'
});
export default pool;
Update src/index.ts to run migrations:
import express from 'express';
import pool from './db/pool';
import { runMigrations } from './db/migrations';
const app = express();
app.use(express.json());
// Routes will go here
async function start() {
try {
await runMigrations(pool);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
} catch (err) {
console.error('Failed to start server:', err);
process.exit(1);
}
}
start();
npm run dev logs "Migrations completed successfully".docker ps should show a postgres container, or psql -U postgres should work.Create src/routes/webhooks.ts:
import { Router, Request, Response } from 'express';
import pool from '../db/pool';
import { v4 as uuidv4 } from 'uuid';
const router = Router();
const VALID_EVENT_TYPES = ['order.created', 'order.updated', 'user.signed_up'];
// POST /webhooks
router.post('/', async (req: Request, res: Response) => {
const { url, secret, event_types } = req.body;
// Validate
if (!url || !secret) {
return res.status(400).json({ error: 'url and secret required' });
}
if (!Array.isArray(event_types) || event_types.length === 0) {
return res.status(400).json({ error: 'event_types must be non-empty array' });
}
if (!event_types.every(t => VALID_EVENT_TYPES.includes(t))) {
return res.status(400).json({ error: `event_types must be one of: ${VALID_EVENT_TYPES.join(', ')}` });
}
try {
const result = await pool.query(
'INSERT INTO webhooks (url, secret, event_types) VALUES ($1, $2, $3) RETURNING *',
[url, secret, event_types]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to create webhook' });
}
});
// GET /webhooks/:id
router.get('/:id', async (req: Request, res: Response) => {
try {
const result = await pool.query(
'SELECT * FROM webhooks WHERE id = $1',
[req.params.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Webhook not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to fetch webhook' });
}
});
export default router;
Update src/index.ts to use the router:
import webhooksRouter from './routes/webhooks';
app.use('/webhooks', webhooksRouter);
curl -X POST http://localhost:3000/webhooks \
-H 'Content-Type: application/json' \
-d '{
"url": "https://example.com/webhook",
"secret": "my-secret",
"event_types": ["order.created"]
}'
Should return 201 with the webhook ID.Create src/routes/events.ts:
import { Router, Request, Response } from 'express';
import pool from '../db/pool';
const router = Router();
const VALID_EVENT_TYPES = ['order.created', 'order.updated', 'user.signed_up'];
// POST /events
router.post('/', async (req: Request, res: Response) => {
const { type, payload } = req.body;
// Validate
if (!type || !VALID_EVENT_TYPES.includes(type)) {
return res.status(400).json({ error: `type must be one of: ${VALID_EVENT_TYPES.join(', ')}` });
}
if (!payload || typeof payload !== 'object') {
return res.status(400).json({ error: 'payload must be a JSON object' });
}
try {
const result = await pool.query(
'INSERT INTO events (type, payload) VALUES ($1, $2) RETURNING *',
[type, payload]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to create event' });
}
});
export default router;
Update src/index.ts:
import eventsRouter from './routes/events';
app.use('/events', eventsRouter);
curl -X POST http://localhost:3000/events \
-H 'Content-Type: application/json' \
-d '{
"type": "order.created",
"payload": {"order_id": 123, "amount": 99.99}
}'
Should return 201 with the event ID.git add -A git commit -m "feat: implement core webhook and event API - POST /webhooks: register webhook endpoints - POST /events: emit events to the system - Database migrations for webhooks, events, deliveries - Input validation and proper HTTP status codes" git push origin main
git log --oneline shows your commit.You now have the basic API contract. Next, you'll implement reliable delivery with retries and exponential backoff. Head to Module 03.