API Patterns
Common API patterns and conventions used in Mavric DevEnv projects.
RESTful Conventions
Resource Naming
| Pattern |
Example |
Description |
| Plural nouns |
/users, /projects |
Resource collections |
| Kebab-case |
/user-settings |
Multi-word resources |
| Nested resources |
/users/:id/posts |
Related resources |
HTTP Methods
| Method |
Purpose |
Example |
GET |
Read resource(s) |
GET /users |
POST |
Create resource |
POST /users |
PUT |
Replace resource |
PUT /users/:id |
PATCH |
Update resource |
PATCH /users/:id |
DELETE |
Delete resource |
DELETE /users/:id |
Success Response
{
"success": true,
"data": {
"id": "123",
"name": "Project Alpha",
"createdAt": "2024-01-15T10:30:00Z"
}
}
{
"success": true,
"data": [
{ "id": "1", "name": "Project A" },
{ "id": "2", "name": "Project B" }
],
"meta": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8
}
}
Error Response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": {
"email": "Invalid email format",
"name": "Name is required"
}
}
}
Status Codes
Success Codes
| Code |
Meaning |
Use Case |
200 |
OK |
Successful GET, PUT, PATCH, DELETE |
201 |
Created |
Successful POST |
204 |
No Content |
Successful DELETE (no body) |
Client Error Codes
| Code |
Meaning |
Use Case |
400 |
Bad Request |
Invalid request format |
401 |
Unauthorized |
Missing/invalid auth |
403 |
Forbidden |
Insufficient permissions |
404 |
Not Found |
Resource doesn't exist |
409 |
Conflict |
Duplicate resource |
422 |
Unprocessable |
Validation errors |
Server Error Codes
| Code |
Meaning |
Use Case |
500 |
Internal Error |
Unexpected server error |
503 |
Service Unavailable |
Maintenance/overload |
Authentication Patterns
Session-Based Auth
BetterAuth uses session-based authentication:
// Check authentication
const session = await auth.getSession(request);
if (!session) {
throw new UnauthorizedException();
}
GET /api/projects HTTP/1.1
Cookie: session=abc123...
Protected Routes
@UseGuards(AuthGuard)
@Get('projects')
async getProjects(@CurrentUser() user: User) {
return this.projectService.findByUser(user.id);
}
Multi-Tenancy Patterns
Organization Scoping
Critical: Every query must be scoped by organization.
// ✅ Correct: Scoped by organization
async getProjects(orgId: string) {
return this.projectRepo.find({
where: { organizationId: orgId }
});
}
// ❌ Wrong: Exposes all data
async getProjects() {
return this.projectRepo.find();
}
Request Context
@Injectable()
export class OrganizationMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const orgId = req.headers['x-organization-id'];
req.organizationId = orgId;
next();
}
}
Validation Patterns
DTO Validation
import { IsString, IsEmail, MinLength, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(2)
name: string;
@IsString()
@MinLength(8)
password: string;
@IsOptional()
@IsString()
avatar?: string;
}
Controller Validation
@Post()
async create(@Body() dto: CreateUserDto) {
// Validation happens automatically via ValidationPipe
return this.userService.create(dto);
}
Zod Validation (Frontend)
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email('Invalid email'),
name: z.string().min(2, 'Name too short'),
password: z.string().min(8, 'Password too short'),
});
type CreateUserInput = z.infer<typeof createUserSchema>;
Error Handling Patterns
Custom Error Classes
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number,
public code: string,
public details?: Record<string, unknown>
) {
super(message);
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(
`${resource} with id ${id} not found`,
404,
'NOT_FOUND'
);
}
}
export class ValidationError extends AppError {
constructor(details: Record<string, string>) {
super(
'Validation failed',
422,
'VALIDATION_ERROR',
details
);
}
}
Global Exception Filter
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
if (exception instanceof AppError) {
return response.status(exception.statusCode).json({
success: false,
error: {
code: exception.code,
message: exception.message,
details: exception.details,
},
});
}
// Unexpected error
return response.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
},
});
}
}
Query Parameters
GET /api/projects?page=2&limit=20&sort=createdAt&order=desc
Implementation
interface PaginationParams {
page?: number;
limit?: number;
sort?: string;
order?: 'asc' | 'desc';
}
async findAll(params: PaginationParams) {
const { page = 1, limit = 20, sort = 'createdAt', order = 'desc' } = params;
const [data, total] = await this.repo.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { [sort]: order },
});
return {
data,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}
Filtering Patterns
Query String Filters
GET /api/projects?status=active&createdAfter=2024-01-01
Implementation
interface ProjectFilters {
status?: string;
createdAfter?: string;
search?: string;
}
async findAll(filters: ProjectFilters, orgId: string) {
const query = this.repo.createQueryBuilder('project')
.where('project.organizationId = :orgId', { orgId });
if (filters.status) {
query.andWhere('project.status = :status', { status: filters.status });
}
if (filters.createdAfter) {
query.andWhere('project.createdAt >= :date', { date: filters.createdAfter });
}
if (filters.search) {
query.andWhere('project.name ILIKE :search', { search: `%${filters.search}%` });
}
return query.getMany();
}
Relationship Patterns
Eager Loading
// Load project with members
const project = await this.projectRepo.findOne({
where: { id },
relations: ['members', 'owner'],
});
Nested Resources
// GET /projects/:projectId/tasks
@Get(':projectId/tasks')
async getProjectTasks(@Param('projectId') projectId: string) {
return this.taskService.findByProject(projectId);
}
// POST /projects/:projectId/tasks
@Post(':projectId/tasks')
async createTask(
@Param('projectId') projectId: string,
@Body() dto: CreateTaskDto
) {
return this.taskService.create({ ...dto, projectId });
}
Rate Limiting
Configuration
import rateLimit from 'express-rate-limit';
// General API limit
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
message: { error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
});
// Stricter auth limit
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
message: { error: { code: 'RATE_LIMITED', message: 'Too many login attempts' } },
});
app.use('/api/', apiLimiter);
app.use('/api/auth/', authLimiter);
CORS Configuration
// Specific origins (production)
app.use(cors({
origin: [
'https://app.example.com',
'https://admin.example.com',
],
credentials: true,
}));
// Development
app.use(cors({
origin: 'http://localhost:3000',
credentials: true,
}));