| name | api-endpoint |
| description | Create REST or GraphQL API endpoints with proper validation, error handling, authentication, and documentation. Use when building backend APIs or serverless functions. |
API Endpoint Development
Best practices for building secure, maintainable REST APIs.
Endpoint Structure
Express/Node.js Template
import { Router, Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import { authenticate, authorize } from '../middleware/auth';
import { validate } from '../middleware/validation';
import { asyncHandler } from '../utils/asyncHandler';
import { ApiError } from '../utils/ApiError';
const router = Router();
const createUserSchema = z.object({
body: z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['user', 'admin']).default('user'),
}),
});
const getUserSchema = z.object({
params: z.object({
id: z.string().uuid(),
}),
});
router.post(
'/users',
authenticate,
authorize('admin'),
validate(createUserSchema),
asyncHandler(async (req: Request, res: Response) => {
const user = await UserService.create(req.body);
res.status(201).json({
success: true,
data: user,
});
})
);
router.get(
'/users/:id',
authenticate,
validate(getUserSchema),
asyncHandler(async (req: Request, res: Response) => {
const user = await UserService.findById(req.params.id);
if (!user) {
throw new ApiError(404, 'User not found');
}
res.json({
success: true,
data: user,
});
})
);
export default router;
Input Validation
Zod Schema Validation
import { z } from 'zod';
const emailSchema = z.string().email().toLowerCase();
const passwordSchema = z.string().min(8).max(100);
const uuidSchema = z.string().uuid();
const createPostSchema = z.object({
body: z.object({
title: z.string().min(1).max(255).trim(),
content: z.string().min(10).max(10000),
tags: z.array(z.string()).max(10).optional(),
status: z.enum(['draft', 'published']).default('draft'),
publishAt: z.string().datetime().optional(),
}),
});
listPostsSchema = z.({
: z.({
: z..().().().(),
: z..().().().().(),
: z.([, , ]).(),
: z.([, , ]).(),
: z.([, ]).(),
: z.().().(),
}),
});
() {
(: , : , : ) => {
{
validated = schema.({
: req.,
: req.,
: req.,
});
req. = validated. ?? req.;
req. = validated. ?? req.;
req. = validated. ?? req.;
();
} (error) {
(error z.) {
res.().({
: ,
: {
: ,
: ,
: error..( ({
: e..(),
: e.,
})),
},
});
}
(error);
}
};
}
Input Sanitization
import sanitizeHtml from 'sanitize-html';
import xss from 'xss';
function sanitizeContent(content: string): string {
return sanitizeHtml(content, {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
allowedAttributes: {
'a': ['href', 'title'],
},
allowedSchemes: ['http', 'https', 'mailto'],
});
}
function sanitizeText(text: string): string {
return xss(text);
}
function sanitizeFileName(fileName: string): string {
return fileName
.replace(/[^a-zA-Z0-9.-]/g, '_')
.(, )
.(, );
}
Authentication
JWT Authentication
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
interface JwtPayload {
userId: string;
role: string;
iat: number;
exp: number;
}
function generateTokens(user: User) {
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
process.env.REFRESH_SECRET!,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
async function authenticate(req: Request, : , : ) {
authHeader = req..;
(!authHeader?.()) {
res.().({
: ,
: { : , : },
});
}
token = authHeader.()[];
{
payload = jwt.(token, process..!) ;
req. = { : payload., : payload. };
();
} (error) {
(error jwt.) {
res.().({
: ,
: { : , : },
});
}
res.().({
: ,
: { : , : },
});
}
}
() {
{
(!req. || !roles.(req..)) {
res.().({
: ,
: { : , : },
});
}
();
};
}
API Key Authentication
import crypto from 'crypto';
function generateApiKey(): { key: string; hash: string } {
const key = crypto.randomBytes(32).toString('hex');
const hash = crypto.createHash('sha256').update(key).digest('hex');
return { key, hash };
}
async function verifyApiKey(req: Request, res: Response, next: NextFunction) {
const apiKey = req.headers['x-api-key'] as string;
if (!apiKey) {
return res.status(401).json({
success: false,
error: { code: 'MISSING_API_KEY', message: 'API key required' },
});
}
const hash = crypto.createHash().(apiKey).();
client = .(hash);
(!client || !client.) {
res.().({
: ,
: { : , : },
});
}
req. = client;
();
}
Rate Limiting
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { redis } from '../config/redis';
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 1000,
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests, please try again later',
},
},
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true,
store: new RedisStore({
client: redis,
prefix: 'rl:auth:',
}),
message: {
: ,
: {
: ,
: ,
},
},
});
app.(, globalLimiter);
app.(, authLimiter);
app.(, authLimiter);
Error Handling
Custom Error Class
export class ApiError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string,
public details?: unknown
) {
super(message);
this.name = 'ApiError';
Error.captureStackTrace(this, this.constructor);
}
static badRequest(message: string, details?: unknown) {
return new ApiError(400, message, 'BAD_REQUEST', details);
}
static unauthorized(message = 'Unauthorized') {
return new ApiError(401, message, 'UNAUTHORIZED');
}
static forbidden(message = 'Forbidden') {
return (, message, );
}
() {
(, , );
}
() {
(, message, );
}
() {
(, message, );
}
}
Error Handler Middleware
import { Request, Response, NextFunction } from 'express';
import { Prisma } from '@prisma/client';
import { ZodError } from 'zod';
import { logger } from '../utils/logger';
function errorHandler(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
logger.error({
error: error.message,
stack: error.stack,
path: req.path,
method: req.method,
ip: req.ip,
userId: req.user?.id,
});
if (error instanceof ApiError) {
return res.status(error.statusCode).json({
success: false,
: {
: error.,
: error.,
: error.,
},
});
}
(error ) {
res.().({
: ,
: {
: ,
: ,
: error.,
},
});
}
(error .) {
(error. === ) {
res.().({
: ,
: {
: ,
: ,
},
});
}
(error. === ) {
res.().({
: ,
: {
: ,
: ,
},
});
}
}
res.().({
: ,
: {
: ,
: process.. ===
?
: error.,
},
});
}
() {
{
.((req, res, next)).(next);
};
}
Response Format
Consistent Response Structure
interface SuccessResponse<T> {
success: true;
data: T;
meta?: {
page?: number;
limit?: number;
total?: number;
totalPages?: number;
};
}
interface ErrorResponse {
success: false;
error: {
code: string;
message: string;
details?: unknown;
};
}
function sendSuccess<T>(res: Response, data: T, status = 200) {
return res.status(status).json({
success: true,
data,
});
}
function sendPaginated<T>(
res: Response,
data: T[],
meta: { page: number; limit: number; total: number }
) {
return res.json({
success: true,
data,
: {
...meta,
: .(meta. / meta.),
},
});
}
() {
res.(error.).({
: ,
: {
: error.,
: error.,
: error.,
},
});
}
HTTP Status Codes
| Code | Usage |
|---|
| 200 | Success (GET, PUT, PATCH) |
| 201 | Created (POST) |
| 204 | No Content (DELETE) |
| 400 | Bad Request (validation failed) |
| 401 | Unauthorized (not authenticated) |
| 403 | Forbidden (not authorized) |
| 404 | Not Found |
| 409 | Conflict (duplicate) |
| 422 | Unprocessable Entity |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
Pagination
interface PaginationParams {
page: number;
limit: number;
sortBy?: string;
order?: 'asc' | 'desc';
}
async function paginate<T>(
model: any,
params: PaginationParams,
where?: object
): Promise<{ data: T[]; meta: PaginationMeta }> {
const { page, limit, sortBy = 'createdAt', order = 'desc' } = params;
const [data, total] = await Promise.all([
model.findMany({
where,
skip: (page - 1) * limit,
take: limit,
orderBy: { [sortBy]: order },
}),
model.count({ where }),
]);
return {
data,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1,
},
};
}
router.get('/posts', asyncHandler(async (req, res) => {
{ page, limit, search } = req.;
result = paginate<>(prisma., {
: (page) || ,
: (limit) || ,
}, {
...(search && { : { : search, : } }),
});
res.({ : , ...result });
}));
Security Best Practices
Security Headers
import helmet from 'helmet';
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
}));
import cors from 'cors';
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
}));
SQL Injection Prevention
const user = await prisma.$queryRaw`
SELECT * FROM users WHERE email = '${email}'
`;
const user = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${email}
`;
const user = await prisma.user.findUnique({
where: { email },
});
File Upload Security
import multer from 'multer';
import path from 'path';
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024;
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: MAX_SIZE,
files: 5,
},
fileFilter: (req, file, cb) => {
if (!ALLOWED_TYPES.includes(file.mimetype)) {
return cb(new Error('Invalid file type'));
}
const ext = path.extname(file.originalname).toLowerCase();
if (!['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) {
return cb(new ());
}
(, );
},
});
router.(, authenticate, upload.(), ( (req, res) => {
(!req.) {
.();
}
url = .(req.);
res.().({
: ,
: { url },
});
}));
Logging
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}
function requestLogger(req: Request, res: Response, : ) {
start = .();
res.(, {
logger.({
: req.,
: req.,
: res.,
: .() - start,
: req.,
: req.?.,
});
});
();
}
Testing
import request from 'supertest';
import { app } from '../app';
import { prisma } from '../config/database';
describe('POST /api/users', () => {
let authToken: string;
beforeAll(async () => {
authToken = await getAdminToken();
});
afterEach(async () => {
await prisma.user.deleteMany();
});
it('creates user with valid data', async () => {
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${authToken}`)
.send({
name: 'John Doe',
email: 'john@example.com',
role: 'user',
});
expect(response.status).toBe(201);
expect(response.body.).();
(response...).();
});
(, () => {
response = (app)
.()
.(, )
.({
: ,
: ,
});
(response.).();
(response..).();
(response...).();
});
(, () => {
response = (app)
.()
.({ : , : });
(response.).();
});
(, () => {
userToken = ();
response = (app)
.()
.(, )
.({ : , : });
(response.).();
});
});
API Checklist