| name | innozverse-api-style |
| description | 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. |
innozverse API Development Style
When developing API endpoints for innozverse, follow these patterns and conventions.
Fastify Basics
Route Registration
// 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 });
});
}
Register in Index
// apps/api/src/index.ts
import { usersRoutes } from './routes/v1/users';
fastify.register(usersRoutes, { prefix: '/v1' });
Response Patterns
Success Response
return reply.code(200).send({
status: 'ok',
data: { /* ... */ }
});
Created Response
return reply.code(201).send({
status: 'created',
data: { id: newId }
});
Error Response
return reply.code(400).send({
error: 'ValidationError',
message: 'Invalid input',
statusCode: 400
});
Type Safety
Define Types in @innozverse/shared
// packages/shared/src/types.ts
export interface User {
id: string;
name: string;
email: string;
}
export interface UserResponse {
status: 'ok';
data: User;
}
Use in API
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
});
});
Validation with Zod
Define Schema in @innozverse/shared
// packages/shared/src/schemas.ts
import { z } from 'zod';
export const userSchema = z.object({
name: z.string().min(1),
email: z.string().email()
});
Use in API
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
});
}
});
Error Handling
Global Error Handler
// 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
});
});
Throwing Errors
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 };
});
Async/Await
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 });
});
});
RESTful Conventions
Resource Naming
- Plural nouns:
/users,/posts - Nested resources:
/users/:userId/posts
HTTP Methods
GET /resource- List allGET /resource/:id- Get onePOST /resource- CreatePUT /resource/:id- ReplacePATCH /resource/:id- UpdateDELETE /resource/:id- Delete
Status Codes
200- 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 Error
Versioning
Always version API routes:
// ✅ Good
fastify.register(v1Routes, { prefix: '/v1' });
fastify.register(v2Routes, { prefix: '/v2' });
// ❌ Bad
fastify.register(routes); // No version
CORS Configuration
import cors from '@fastify/cors';
await fastify.register(cors, {
origin: process.env.CORS_ORIGIN || '*',
credentials: true
});
Environment Variables
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; // ✅
Logging
// Use Fastify's built-in logger
fastify.log.info('Server starting');
fastify.log.error({ err: error }, 'Error occurred');
fastify.log.debug({ data }, 'Debug info');
Health Check
Always maintain a health check endpoint:
fastify.get('/health', async () => ({
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.API_VERSION || '1.0.0'
}));
Testing (Future)
// 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');
});
});
Database Patterns (Future)
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] };
});
Best Practices
Single Responsibility
Each route file handles one resource:
routes/v1/
├── users.ts # User management
├── posts.ts # Post management
└── comments.ts # Comment management
DRY Principles
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 };
});
Graceful Shutdown
process.on('SIGTERM', async () => {
await fastify.close();
process.exit(0);
});
Anti-Patterns to Avoid
❌ 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' });