Skip to content

Add an API Endpoint

Add request/response schemas to shared/src/types/api.ts:

import { z } from 'zod';
export const CreateWidgetRequest = z.object({
name: z.string().min(1),
});
export type CreateWidgetRequest = z.infer<typeof CreateWidgetRequest>;
export const CreateWidgetResponse = z.object({
widget: z.object({
id: z.string().uuid(),
name: z.string(),
created_at: z.string(),
}),
});
export type CreateWidgetResponse = z.infer<typeof CreateWidgetResponse>;

Add the route to shared/src/routes.ts:

export enum ApiRoutes {
// ...existing routes
Widgets = '/api/widgets',
WidgetById = '/api/widgets/:id',
}

Create server/src/routes/widgets.ts. Validate the request body with the Zod schema:

import type { FastifyInstance } from 'fastify';
import { ApiRoutes, CreateWidgetRequest } from '@codecosts/shared';
import type { CreateWidgetResponse } from '@codecosts/shared';
import logger from '../logger';
export default async function widgetRoutes(app: FastifyInstance) {
app.post(ApiRoutes.Widgets, async (req, reply) => {
const parsed = CreateWidgetRequest.safeParse(req.body);
if (!parsed.success) {
reply.code(400).send({ error: 'Invalid request', details: parsed.error.message });
return;
}
const { name } = parsed.data;
// ... implementation
logger.info({ component: 'widgets', action: 'created', userId: req.user?.id }, 'Widget created');
return { widget } satisfies CreateWidgetResponse;
});
}

Inside the protected routes plugin:

app.register(async (protectedRoutes) => {
protectedRoutes.addHook('onRequest', authHook);
protectedRoutes.register(widgetRoutes);
});

Add tests in server/src/__tests__/widgets.test.ts covering:

  • Happy path (201 with correct body)
  • Auth required (401 without token)
  • Validation (400 with bad input — Zod rejects it)
  • Not found (404 for missing resources)
Terminal window
bun run typecheck
bun run lint
bun test 2>&1 | tee /tmp/test.txt