API Architecture
Technology Stack
Next.js API Routes
- File-based routing
- Built-in middleware
- TypeScript support
- Edge runtime support
Authentication
- Clerk integration
- JWT tokens
- Role-based access
- Multi-tenant security
API Design Principles
RESTful Design
- Use HTTP methods appropriately (GET, POST, PUT, DELETE)
- Implement consistent URL patterns
- Return appropriate HTTP status codes
- Use proper content types and headers
- Follow RESTful resource naming conventions
Multi-Tenancy
All API endpoints automatically scope data by team/organization to ensure data isolation and security.
- Team Scoping: All operations include teamId validation
- Data Isolation: Complete separation between organizations
- Security: Row-level security and access controls
- Performance: Optimized for multi-tenant queries
API Route Structure
File-Based Routing
Copy
apps/app/app/api/
├── customers/
│ ├── route.ts # GET /api/customers, POST /api/customers
│ └── [id]/
│ ├── route.ts # GET /api/customers/[id], PUT /api/customers/[id], DELETE /api/customers/[id]
│ └── orders/
│ └── route.ts # GET /api/customers/[id]/orders
├── orders/
│ ├── route.ts # GET /api/orders, POST /api/orders
│ └── [id]/
│ ├── route.ts # GET /api/orders/[id], PUT /api/orders/[id], DELETE /api/orders/[id]
│ └── items/
│ └── route.ts # GET /api/orders/[id]/items, POST /api/orders/[id]/items
├── products/
│ ├── route.ts # GET /api/products, POST /api/products
│ └── [id]/
│ └── route.ts # GET /api/products/[id], PUT /api/products/[id], DELETE /api/products/[id]
└── health/
└── route.ts # GET /api/health
Route Handler Structure
Copy
// apps/app/app/api/customers/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs';
import { z } from 'zod';
import { database } from '@repo/database';
// GET /api/customers
export async function GET(request: NextRequest) {
try {
// Authentication
const { userId, orgId } = auth();
if (!userId || !orgId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Query parameters
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const search = searchParams.get('search') || '';
// Database query
const customers = await database.customer.findMany({
where: {
teamId: orgId,
...(search && {
OR: [
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } }
]
})
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit
});
const total = await database.customer.count({
where: {
teamId: orgId,
...(search && {
OR: [
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } }
]
})
}
});
return NextResponse.json({
data: customers,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('Error fetching customers:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// POST /api/customers
export async function POST(request: NextRequest) {
try {
// Authentication
const { userId, orgId } = auth();
if (!userId || !orgId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Request validation
const body = await request.json();
const customerSchema = z.object({
email: z.string().email(),
firstName: z.string().min(1).max(50),
lastName: z.string().min(1).max(50),
phone: z.string().optional(),
address: z.object({
street: z.string().optional(),
city: z.string().optional(),
state: z.string().optional(),
zipCode: z.string().optional(),
country: z.string().optional()
}).optional()
});
const validatedData = customerSchema.parse(body);
// Check for existing customer
const existingCustomer = await database.customer.findUnique({
where: {
email: validatedData.email,
teamId: orgId
}
});
if (existingCustomer) {
return NextResponse.json(
{ error: 'Customer with this email already exists' },
{ status: 409 }
);
}
// Create customer
const customer = await database.customer.create({
data: {
...validatedData,
teamId: orgId
}
});
return NextResponse.json({ data: customer }, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation error', details: error.errors },
{ status: 400 }
);
}
console.error('Error creating customer:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Authentication and Authorization
Clerk Integration
Authentication Middleware
Copy
// lib/auth.ts
import { auth } from '@clerk/nextjs';
import { NextRequest } from 'next/server';
export async function requireAuth(request: NextRequest) {
const { userId, orgId } = auth();
if (!userId) {
throw new Error('Authentication required');
}
if (!orgId) {
throw new Error('Organization membership required');
}
return { userId, orgId };
}
export async function requireRole(request: NextRequest, allowedRoles: string[]) {
const { userId, orgId } = await requireAuth(request);
// Check user role in organization
const userMembership = await database.teamMembership.findFirst({
where: {
userId,
teamId: orgId
},
include: {
role: true
}
});
if (!userMembership || !allowedRoles.includes(userMembership.role.name)) {
throw new Error('Insufficient permissions');
}
return { userId, orgId, role: userMembership.role.name };
}
Role-Based Access Control
Copy
// API route with role-based access
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Require admin role for deletion
const { userId, orgId } = await requireRole(request, ['admin', 'owner']);
const { id } = await params;
// Soft delete customer
const customer = await database.customer.update({
where: {
id,
teamId: orgId
},
data: {
deletedAt: new Date()
}
});
return NextResponse.json({ data: customer });
} catch (error) {
if (error.message === 'Authentication required') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
if (error.message === 'Insufficient permissions') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
console.error('Error deleting customer:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Request Validation
Schema Validation with Zod
Copy
// lib/validation/customer.ts
import { z } from 'zod';
export const customerCreateSchema = z.object({
email: z.string().email('Invalid email address'),
firstName: z.string()
.min(1, 'First name is required')
.max(50, 'First name must be less than 50 characters'),
lastName: z.string()
.min(1, 'Last name is required')
.max(50, 'Last name must be less than 50 characters'),
phone: z.string()
.regex(/^\+?[\d\s\-\(\)]+$/, 'Invalid phone number format')
.optional(),
address: z.object({
street: z.string().optional(),
city: z.string().optional(),
state: z.string().optional(),
zipCode: z.string().optional(),
country: z.string().optional()
}).optional()
});
export const customerUpdateSchema = customerCreateSchema.partial();
export const customerQuerySchema = z.object({
page: z.string().regex(/^\d+$/).transform(Number).default('1'),
limit: z.string().regex(/^\d+$/).transform(Number).default('10'),
search: z.string().optional(),
sortBy: z.enum(['firstName', 'lastName', 'email', 'createdAt']).default('createdAt'),
sortOrder: z.enum(['asc', 'desc']).default('desc')
});
Validation Middleware
Copy
// lib/validation/middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
export function validateRequest<T>(schema: z.ZodSchema<T>) {
return async (request: NextRequest): Promise<T> => {
try {
const body = await request.json();
return schema.parse(body);
} catch (error) {
if (error instanceof z.ZodError) {
throw new ValidationError('Invalid request data', error.errors);
}
throw error;
}
};
}
export class ValidationError extends Error {
constructor(message: string, public details: z.ZodIssue[]) {
super(message);
this.name = 'ValidationError';
}
}
Error Handling
Centralized Error Handling
Copy
// lib/errors.ts
export class APIError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string
) {
super(message);
this.name = 'APIError';
}
}
export class ValidationError extends APIError {
constructor(message: string, public details: any) {
super(400, message, 'VALIDATION_ERROR');
}
}
export class NotFoundError extends APIError {
constructor(resource: string) {
super(404, `${resource} not found`, 'NOT_FOUND');
}
}
export class UnauthorizedError extends APIError {
constructor() {
super(401, 'Unauthorized', 'UNAUTHORIZED');
}
}
export class ForbiddenError extends APIError {
constructor() {
super(403, 'Forbidden', 'FORBIDDEN');
}
}
Error Response Format
Copy
// lib/error-handler.ts
import { NextResponse } from 'next/server';
import { APIError } from './errors';
export function handleAPIError(error: unknown) {
console.error('API Error:', error);
if (error instanceof APIError) {
return NextResponse.json(
{
error: error.message,
code: error.code,
statusCode: error.statusCode
},
{ status: error.statusCode }
);
}
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: 'Validation error',
code: 'VALIDATION_ERROR',
details: error.errors
},
{ status: 400 }
);
}
return NextResponse.json(
{
error: 'Internal server error',
code: 'INTERNAL_ERROR'
},
{ status: 500 }
);
}
Response Formatting
Standard Response Format
Copy
// lib/response.ts
import { NextResponse } from 'next/server';
export interface APIResponse<T = any> {
data?: T;
error?: string;
code?: string;
pagination?: {
page: number;
limit: number;
total: number;
pages: number;
};
meta?: {
timestamp: string;
requestId: string;
};
}
export function successResponse<T>(
data: T,
status: number = 200,
pagination?: APIResponse['pagination']
): NextResponse<APIResponse<T>> {
return NextResponse.json({
data,
pagination,
meta: {
timestamp: new Date().toISOString(),
requestId: crypto.randomUUID()
}
}, { status });
}
export function errorResponse(
error: string,
status: number = 500,
code?: string
): NextResponse<APIResponse> {
return NextResponse.json({
error,
code,
meta: {
timestamp: new Date().toISOString(),
requestId: crypto.randomUUID()
}
}, { status });
}
Pagination
Copy
// lib/pagination.ts
export interface PaginationOptions {
page: number;
limit: number;
total: number;
}
export function createPaginationResponse(options: PaginationOptions) {
const { page, limit, total } = options;
const pages = Math.ceil(total / limit);
return {
page,
limit,
total,
pages,
hasNext: page < pages,
hasPrev: page > 1
};
}
// Usage in API route
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const [items, total] = await Promise.all([
database.customer.findMany({
where: { teamId },
skip: (page - 1) * limit,
take: limit
}),
database.customer.count({ where: { teamId } })
]);
return successResponse(items, 200, createPaginationResponse({
page,
limit,
total
}));
}
Performance Optimization
Caching Strategy
Copy
// lib/cache.ts
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
export async function getCachedData<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number = 300
): Promise<T> {
try {
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const data = await fetcher();
await redis.setex(key, ttl, JSON.stringify(data));
return data;
} catch (error) {
console.error('Cache error:', error);
return await fetcher();
}
}
// Usage in API route
export async function GET(request: NextRequest) {
const { orgId } = await requireAuth(request);
const cacheKey = `customers:${orgId}`;
const customers = await getCachedData(
cacheKey,
() => database.customer.findMany({
where: { teamId: orgId }
}),
300 // 5 minutes
);
return successResponse(customers);
}
Database Query Optimization
Copy
// Optimized query with proper indexing
export async function GET(request: NextRequest) {
const { orgId } = await requireAuth(request);
const { searchParams } = new URL(request.url);
const where = {
teamId: orgId,
deletedAt: null
};
// Add search filter if provided
const search = searchParams.get('search');
if (search) {
where.OR = [
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } }
];
}
// Use select to limit returned fields
const customers = await database.customer.findMany({
where,
select: {
id: true,
email: true,
firstName: true,
lastName: true,
createdAt: true
},
orderBy: { createdAt: 'desc' },
take: 50
});
return successResponse(customers);
}
API Documentation
OpenAPI Specification
Copy
// lib/openapi.ts
export const openAPISpec = {
openapi: '3.0.0',
info: {
title: 'DeelRx CRM API',
version: '1.0.0',
description: 'API for DeelRx CRM system'
},
servers: [
{
url: 'https://api.deelrxcrm.app',
description: 'Production server'
},
{
url: 'http://localhost:3000',
description: 'Development server'
}
],
paths: {
'/api/customers': {
get: {
summary: 'List customers',
parameters: [
{
name: 'page',
in: 'query',
schema: { type: 'integer', default: 1 }
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 }
}
],
responses: {
'200': {
description: 'List of customers',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: {
type: 'array',
items: { $ref: '#/components/schemas/Customer' }
},
pagination: { $ref: '#/components/schemas/Pagination' }
}
}
}
}
}
}
}
}
},
components: {
schemas: {
Customer: {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string', format: 'email' },
firstName: { type: 'string' },
lastName: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' }
}
},
Pagination: {
type: 'object',
properties: {
page: { type: 'integer' },
limit: { type: 'integer' },
total: { type: 'integer' },
pages: { type: 'integer' }
}
}
}
}
};
API Testing
Copy
// tests/api/customers.test.ts
import { describe, it, expect } from 'vitest';
describe('Customers API', () => {
it('should list customers', async () => {
const response = await fetch('/api/customers', {
headers: {
'Authorization': 'Bearer test-token'
}
});
expect(response.status).toBe(200);
const data = await response.json();
expect(data.data).toBeDefined();
expect(Array.isArray(data.data)).toBe(true);
});
it('should create a customer', async () => {
const customerData = {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe'
};
const response = await fetch('/api/customers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test-token'
},
body: JSON.stringify(customerData)
});
expect(response.status).toBe(201);
const data = await response.json();
expect(data.data.email).toBe(customerData.email);
});
});
Best Practices
API Design
- Use consistent URL patterns and HTTP methods
- Implement proper error handling and status codes
- Validate all input data with schemas
- Use appropriate HTTP status codes
- Implement rate limiting and security measures
Performance
- Implement caching for frequently accessed data
- Use database query optimization
- Limit response payload sizes
- Use pagination for large datasets
- Monitor API performance and errors
Security
- Authenticate all API requests
- Validate and sanitize all input data
- Implement proper authorization checks
- Use HTTPS for all communications
- Log security events and monitor for attacks
Troubleshooting
Common Issues
Authentication Failures
Authentication Failures
Check Clerk configuration, verify API keys, and ensure proper token validation. Review user permissions and organization membership.
Database Connection Issues
Database Connection Issues
Verify database URL, check connection pool settings, and monitor database performance. Ensure proper indexing for queries.
Performance Issues
Performance Issues
Analyze slow queries, implement caching strategies, and optimize database operations. Monitor API response times and error rates.
Debugging Tools
Copy
# API testing with curl
curl -X GET "http://localhost:3000/api/customers" \
-H "Authorization: Bearer your-token"
# Database query analysis
pnpm db:studio
# API documentation
pnpm docs:dev