Follow API development conventions including RESTful design, Fastify patterns, Zod validation, error handling, and versioning. Use when building API endpoints, adding routes, or working with API code.
When developing API endpoints for innozverse, follow these patterns and conventions.
// apps/api/src/routes/v1/users.ts
import { FastifyInstance } from 'fastify';
export async function usersRoutes(fastify: FastifyInstance) {
fastify.get('/users', async (request, reply) => {
return { users: [] };
});
fastify.get('/users/:id', async (request, reply) => {
const { id } = request.params as { id: string };
return { user: { id } };
});
fastify.post('/users', async (request, reply) => {
const body = request.body;
return reply.code(201).send({ user: body });
});
}
// apps/api/src/index.ts
import { usersRoutes } from './routes/v1/users';
fastify.register(usersRoutes, { prefix: '/v1' });
return reply.code(200).send({
status: 'ok',
data: { /* ... */ }
});
return reply.code(201).send({
status: 'created',
data: { id: newId }
});
return reply.code(400).send({
error: 'ValidationError',
message: 'Invalid input',
statusCode: 400
});
// packages/shared/src/types.ts
export interface User {
id: string;
name: string;
email: string;
}
export interface UserResponse {
status: 'ok';
data: User;
}
import { User, UserResponse } from '@innozverse/shared';
fastify.get<{ Reply: UserResponse }>('/users/:id', async (request, reply) => {
const user: User = { /* ... */ };
return reply.send({
status: 'ok',
data: user
});
});
// packages/shared/src/schemas.ts
import { z } from 'zod';
export const userSchema = z.object({
name: z.string().min(1),
email: z.string().email()
});
import { userSchema } from '@innozverse/shared';
fastify.post('/users', async (request, reply) => {
try {
const validated = userSchema.parse(request.body);
// Use validated data
return reply.code(201).send({ data: validated });
} catch (error) {
return reply.code(400).send({
error: 'ValidationError',
message: error.message
});
}
});
// apps/api/src/index.ts
fastify.setErrorHandler((error, request, reply) => {
fastify.log.error(error);
reply.status(error.statusCode || 500).send({
error: error.name || 'InternalServerError',
message: error.message || 'Something went wrong',
statusCode: error.statusCode || 500
});
});
fastify.get('/users/:id', async (request, reply) => {
const user = await findUser(id);
if (!user) {
return reply.code(404).send({
error: 'NotFound',
message: 'User not found',
statusCode: 404
});
}
return { data: user };
});
Always use async/await, never callbacks:
// โ
Good
fastify.get('/users', async (request, reply) => {
const users = await getUsers();
return { users };
});
// โ Bad
fastify.get('/users', (request, reply) => {
getUsers((err, users) => {
reply.send({ users });
});
});
/users, /posts/users/:userId/postsGET /resource - List allGET /resource/:id - Get onePOST /resource - CreatePUT /resource/:id - ReplacePATCH /resource/:id - UpdateDELETE /resource/:id - Delete200 - Success (GET, PUT, PATCH, DELETE)201 - Created (POST)204 - No Content (DELETE)400 - Bad Request (validation error)401 - Unauthorized403 - Forbidden404 - Not Found409 - Conflict (duplicate)500 - Internal Server ErrorAlways version API routes:
// โ
Good
fastify.register(v1Routes, { prefix: '/v1' });
fastify.register(v2Routes, { prefix: '/v2' });
// โ Bad
fastify.register(routes); // No version
import cors from '@fastify/cors';
await fastify.register(cors, {
origin: process.env.CORS_ORIGIN || '*',
credentials: true
});
const PORT = parseInt(process.env.PORT || '8080', 10);
const NODE_ENV = process.env.NODE_ENV || 'development';
// Never hardcode secrets
const DB_URL = process.env.DATABASE_URL; // โ
const API_KEY = process.env.API_KEY; // โ
// Use Fastify's built-in logger
fastify.log.info('Server starting');
fastify.log.error({ err: error }, 'Error occurred');
fastify.log.debug({ data }, 'Debug info');
Always maintain a health check endpoint:
fastify.get('/health', async () => ({
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.API_VERSION || '1.0.0'
}));
// apps/api/src/routes/__tests__/users.test.ts
import { buildServer } from '../../index';
describe('Users API', () => {
let fastify;
beforeAll(async () => {
fastify = await buildServer();
});
afterAll(async () => {
await fastify.close();
});
test('GET /v1/users returns users list', async () => {
const response = await fastify.inject({
method: 'GET',
url: '/v1/users'
});
expect(response.statusCode).toBe(200);
expect(response.json()).toHaveProperty('users');
});
});
When adding database:
// Use a connection pool
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
// Close connections gracefully
fastify.addHook('onClose', async () => {
await pool.end();
});
// Use in routes
fastify.get('/users/:id', async (request, reply) => {
const { rows } = await pool.query(
'SELECT * FROM users WHERE id = $1',
[request.params.id]
);
if (rows.length === 0) {
return reply.code(404).send({ error: 'Not found' });
}
return { data: rows[0] };
});
Each route file handles one resource:
routes/v1/
โโโ users.ts # User management
โโโ posts.ts # Post management
โโโ comments.ts # Comment management
Extract common logic:
// utils/auth.ts
export async function requireAuth(request, reply) {
const token = request.headers.authorization;
if (!token) {
return reply.code(401).send({ error: 'Unauthorized' });
}
// Verify token
}
// routes/v1/users.ts
fastify.get('/users/me', {
preHandler: requireAuth
}, async (request, reply) => {
return { user: request.user };
});
process.on('SIGTERM', async () => {
await fastify.close();
process.exit(0);
});
โ Don't use any types:
// Bad
fastify.get('/users', async (request: any, reply: any) => {
โ Don't block the event loop:
// Bad
fastify.get('/heavy', async () => {
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i;
}
return { result };
});
โ Don't ignore errors:
// Bad
fastify.get('/users', async () => {
const users = await getUsers().catch(() => []);
return { users };
});
โ Don't expose internal errors to clients:
// Bad
return reply.code(500).send({ error: error.stack });
// Good
fastify.log.error(error);
return reply.code(500).send({ error: 'Internal Server Error' });