Generate production-ready RESTful APIs with best practices
Generates production-ready RESTful APIs with routing, validation, authentication, and OpenAPI documentation.
/plugin marketplace add jeremylongshore/claude-code-plugins-plus/plugin install rest-api-generator@claude-code-plugins-plusAutomatically generate comprehensive RESTful API implementations from schema definitions with proper routing, validation, error handling, authentication, pagination, and OpenAPI documentation following industry best practices.
Use /generate-rest-api when you need to:
DON'T use this when:
This command implements OpenAPI-first design as the primary approach because:
Alternative considered: Code-first with annotations
Alternative considered: GraphQL
Before running this command:
Create comprehensive API contracts with schemas, endpoints, and examples.
Build framework-specific implementations from OpenAPI specs.
Add controllers, services, and data access layers.
Configure authentication, validation, rate limiting, and CORS.
Create interactive docs, client SDKs, and Postman collections.
The command generates:
openapi.yaml - Complete API specificationsrc/controllers/ - Request handlers with business logicsrc/routes/ - RESTful endpoint definitionssrc/models/ - Data models and schemassrc/middleware/ - Auth, validation, rate limitingsrc/services/ - Business logic layertests/ - Integration and unit testsdocs/ - API documentation and examples# openapi.yaml
openapi: 3.0.0
info:
title: E-commerce API
version: 1.0.0
description: RESTful API for e-commerce platform
servers:
- url: https://api.example.com/v1
description: Production server
- url: http://localhost:3000/v1
description: Development server
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Product:
type: object
required:
- name
- price
- inventory
properties:
id:
type: string
format: uuid
readOnly: true
name:
type: string
minLength: 1
maxLength: 200
description:
type: string
maxLength: 2000
price:
type: number
minimum: 0
multipleOf: 0.01
inventory:
type: integer
minimum: 0
category:
type: string
enum: [electronics, clothing, food, books, other]
images:
type: array
items:
type: string
format: uri
metadata:
type: object
additionalProperties:
type: string
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
Error:
type: object
required:
- code
- message
properties:
code:
type: string
message:
type: string
details:
type: object
PaginatedProducts:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Product'
pagination:
type: object
properties:
total:
type: integer
page:
type: integer
perPage:
type: integer
totalPages:
type: integer
links:
type: object
properties:
self:
type: string
next:
type: string
prev:
type: string
first:
type: string
last:
type: string
paths:
/products:
get:
summary: List products
operationId: listProducts
tags:
- Products
parameters:
- name: page
in: query
schema:
type: integer
default: 1
minimum: 1
- name: perPage
in: query
schema:
type: integer
default: 20
minimum: 1
maximum: 100
- name: category
in: query
schema:
type: string
enum: [electronics, clothing, food, books, other]
- name: minPrice
in: query
schema:
type: number
minimum: 0
- name: maxPrice
in: query
schema:
type: number
minimum: 0
- name: search
in: query
schema:
type: string
- name: sort
in: query
schema:
type: string
enum: [name, price, createdAt]
- name: order
in: query
schema:
type: string
enum: [asc, desc]
default: asc
responses:
'200':
description: Products retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedProducts'
'400':
description: Invalid request parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
summary: Create product
operationId: createProduct
tags:
- Products
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
responses:
'201':
description: Product created successfully
headers:
Location:
schema:
type: string
format: uri
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'400':
description: Invalid product data
'401':
description: Unauthorized
'422':
description: Validation error
/products/{productId}:
parameters:
- name: productId
in: path
required: true
schema:
type: string
format: uuid
get:
summary: Get product by ID
operationId: getProduct
tags:
- Products
responses:
'200':
description: Product retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'404':
description: Product not found
put:
summary: Update product
operationId: updateProduct
tags:
- Products
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
responses:
'200':
description: Product updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'400':
description: Invalid product data
'401':
description: Unauthorized
'404':
description: Product not found
patch:
summary: Partial update product
operationId: patchProduct
tags:
- Products
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
minProperties: 1
responses:
'200':
description: Product updated successfully
delete:
summary: Delete product
operationId: deleteProduct
tags:
- Products
security:
- bearerAuth: []
responses:
'204':
description: Product deleted successfully
'401':
description: Unauthorized
'404':
description: Product not found
// src/app.ts - Express application setup
import express, { Application } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import morgan from 'morgan';
import swaggerUi from 'swagger-ui-express';
import { rateLimit } from 'express-rate-limit';
import { errorHandler } from './middleware/errorHandler';
import { notFoundHandler } from './middleware/notFoundHandler';
import { authMiddleware } from './middleware/auth';
import { validationMiddleware } from './middleware/validation';
import { productRouter } from './routes/products';
import { loadOpenAPISpec } from './utils/openapi';
export class App {
public app: Application;
constructor() {
this.app = express();
this.initializeMiddleware();
this.initializeRoutes();
this.initializeErrorHandling();
}
private initializeMiddleware(): void {
// Security middleware
this.app.use(helmet());
this.app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
message: 'Too many requests from this IP'
});
this.app.use('/api/', limiter);
// Body parsing
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Compression
this.app.use(compression());
// Logging
this.app.use(morgan('combined'));
// API Documentation
const openAPISpec = loadOpenAPISpec();
this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openAPISpec));
}
private initializeRoutes(): void {
// Health check
this.app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
// API routes
this.app.use('/v1/products', productRouter);
// Catch-all for API routes
this.app.all('/v1/*', notFoundHandler);
}
private initializeErrorHandling(): void {
this.app.use(errorHandler);
}
}
// src/controllers/productController.ts
import { Request, Response, NextFunction } from 'express';
import { ProductService } from '../services/productService';
import { PaginationParams, FilterParams } from '../types';
import { ApiError } from '../utils/ApiError';
export class ProductController {
constructor(private productService: ProductService) {}
async listProducts(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const pagination: PaginationParams = {
page: parseInt(req.query.page as string) || 1,
perPage: parseInt(req.query.perPage as string) || 20
};
const filters: FilterParams = {
category: req.query.category as string,
minPrice: parseFloat(req.query.minPrice as string),
maxPrice: parseFloat(req.query.maxPrice as string),
search: req.query.search as string,
sort: req.query.sort as string || 'createdAt',
order: req.query.order as 'asc' | 'desc' || 'desc'
};
const result = await this.productService.listProducts(
pagination,
filters
);
// Add HATEOAS links
const baseUrl = `${req.protocol}://${req.get('host')}${req.baseUrl}`;
const links = this.generatePaginationLinks(
baseUrl,
pagination,
result.pagination.totalPages
);
res.json({
...result,
links
});
} catch (error) {
next(error);
}
}
async getProduct(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const { productId } = req.params;
const product = await this.productService.getProduct(productId);
if (!product) {
throw new ApiError(404, 'Product not found');
}
// Add HATEOAS links
const links = {
self: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
update: {
href: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
method: 'PUT'
},
delete: {
href: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
method: 'DELETE'
}
};
res.json({
...product,
_links: links
});
} catch (error) {
next(error);
}
}
async createProduct(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const product = await this.productService.createProduct(req.body);
const location = `${req.protocol}://${req.get('host')}${req.baseUrl}/${product.id}`;
res.status(201)
.location(location)
.json(product);
} catch (error) {
next(error);
}
}
async updateProduct(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const { productId } = req.params;
const product = await this.productService.updateProduct(
productId,
req.body
);
if (!product) {
throw new ApiError(404, 'Product not found');
}
res.json(product);
} catch (error) {
next(error);
}
}
async patchProduct(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const { productId } = req.params;
const product = await this.productService.patchProduct(
productId,
req.body
);
if (!product) {
throw new ApiError(404, 'Product not found');
}
res.json(product);
} catch (error) {
next(error);
}
}
async deleteProduct(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const { productId } = req.params;
const deleted = await this.productService.deleteProduct(productId);
if (!deleted) {
throw new ApiError(404, 'Product not found');
}
res.status(204).send();
} catch (error) {
next(error);
}
}
private generatePaginationLinks(
baseUrl: string,
pagination: PaginationParams,
totalPages: number
): Record<string, string> {
const links: Record<string, string> = {
self: `${baseUrl}?page=${pagination.page}&perPage=${pagination.perPage}`
};
if (pagination.page > 1) {
links.first = `${baseUrl}?page=1&perPage=${pagination.perPage}`;
links.prev = `${baseUrl}?page=${pagination.page - 1}&perPage=${pagination.perPage}`;
}
if (pagination.page < totalPages) {
links.next = `${baseUrl}?page=${pagination.page + 1}&perPage=${pagination.perPage}`;
links.last = `${baseUrl}?page=${totalPages}&perPage=${pagination.perPage}`;
}
return links;
}
}
// src/middleware/validation.ts
import { Request, Response, NextFunction } from 'express';
import Joi from 'joi';
import { ApiError } from '../utils/ApiError';
export function validate(schema: Joi.Schema) {
return (req: Request, res: Response, next: NextFunction) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
throw new ApiError(422, 'Validation error', errors);
}
req.body = value;
next();
};
}
// src/schemas/product.schema.ts
import Joi from 'joi';
export const productSchema = {
create: Joi.object({
name: Joi.string().min(1).max(200).required(),
description: Joi.string().max(2000),
price: Joi.number().min(0).multiple(0.01).required(),
inventory: Joi.number().integer().min(0).required(),
category: Joi.string().valid(
'electronics',
'clothing',
'food',
'books',
'other'
),
images: Joi.array().items(Joi.string().uri()),
metadata: Joi.object().pattern(Joi.string(), Joi.string())
}),
update: Joi.object({
name: Joi.string().min(1).max(200),
description: Joi.string().max(2000),
price: Joi.number().min(0).multiple(0.01),
inventory: Joi.number().integer().min(0),
category: Joi.string().valid(
'electronics',
'clothing',
'food',
'books',
'other'
),
images: Joi.array().items(Joi.string().uri()),
metadata: Joi.object().pattern(Joi.string(), Joi.string())
}).min(1),
query: Joi.object({
page: Joi.number().integer().min(1).default(1),
perPage: Joi.number().integer().min(1).max(100).default(20),
category: Joi.string().valid(
'electronics',
'clothing',
'food',
'books',
'other'
),
minPrice: Joi.number().min(0),
maxPrice: Joi.number().min(0),
search: Joi.string(),
sort: Joi.string().valid('name', 'price', 'createdAt'),
order: Joi.string().valid('asc', 'desc').default('asc')
})
};
# main.py - FastAPI application
from fastapi import FastAPI, HTTPException, Query, Depends, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import JSONResponse
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from typing import Optional, List
from datetime import datetime
from pydantic import BaseModel, Field, validator
import uvicorn
# Create rate limiter
limiter = Limiter(key_func=get_remote_address)
# Create FastAPI app
app = FastAPI(
title="E-commerce API",
version="1.0.0",
description="RESTful API for e-commerce platform",
docs_url="/api-docs",
redoc_url="/redoc"
)
# Add rate limit handler
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# Add middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["*.example.com", "localhost"]
)
# Pydantic models
class ProductBase(BaseModel):
name: str = Field(..., min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=2000)
price: float = Field(..., gt=0, multiple_of=0.01)
inventory: int = Field(..., ge=0)
category: Optional[str] = Field(
None,
regex="^(electronics|clothing|food|books|other)$"
)
images: Optional[List[str]] = []
metadata: Optional[dict] = {}
@validator('price')
def validate_price(cls, v):
if v <= 0:
raise ValueError('Price must be positive')
return round(v, 2)
class ProductCreate(ProductBase):
pass
class ProductUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=2000)
price: Optional[float] = Field(None, gt=0, multiple_of=0.01)
inventory: Optional[int] = Field(None, ge=0)
category: Optional[str] = None
images: Optional[List[str]] = None
metadata: Optional[dict] = None
class Product(ProductBase):
id: str
created_at: datetime
updated_at: datetime
class Config:
orm_mode = True
class PaginationParams(BaseModel):
page: int = Query(1, ge=1, description="Page number")
per_page: int = Query(20, ge=1, le=100, description="Items per page")
class FilterParams(BaseModel):
category: Optional[str] = Query(None, regex="^(electronics|clothing|food|books|other)$")
min_price: Optional[float] = Query(None, ge=0)
max_price: Optional[float] = Query(None, ge=0)
search: Optional[str] = Query(None, max_length=100)
sort: str = Query("created_at", regex="^(name|price|created_at)$")
order: str = Query("desc", regex="^(asc|desc)$")
class PaginatedResponse(BaseModel):
data: List[Product]
pagination: dict
links: dict
# Routes
@app.get("/health")
async def health_check():
"""Health check endpoint."""
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat()
}
@app.get(
"/v1/products",
response_model=PaginatedResponse,
summary="List products",
tags=["Products"]
)
@limiter.limit("100/minute")
async def list_products(
request: Request,
pagination: PaginationParams = Depends(),
filters: FilterParams = Depends()
):
"""
List products with pagination and filtering.
- **page**: Page number (default: 1)
- **per_page**: Items per page (default: 20, max: 100)
- **category**: Filter by category
- **min_price**: Minimum price filter
- **max_price**: Maximum price filter
- **search**: Search in name and description
- **sort**: Sort field (name, price, created_at)
- **order**: Sort order (asc, desc)
"""
# Implement service logic here
products = await product_service.list_products(pagination, filters)
# Generate HATEOAS links
base_url = str(request.url).split('?')[0]
links = generate_pagination_links(
base_url,
pagination.page,
pagination.per_page,
products['total_pages']
)
return PaginatedResponse(
data=products['data'],
pagination=products['pagination'],
links=links
)
@app.get(
"/v1/products/{product_id}",
response_model=Product,
summary="Get product by ID",
tags=["Products"]
)
async def get_product(product_id: str):
"""Get a single product by ID."""
product = await product_service.get_product(product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Product {product_id} not found"
)
return product
@app.post(
"/v1/products",
response_model=Product,
status_code=status.HTTP_201_CREATED,
summary="Create product",
tags=["Products"]
)
@limiter.limit("10/minute")
async def create_product(
request: Request,
product: ProductCreate,
user=Depends(get_current_user)
):
"""Create a new product (requires authentication)."""
new_product = await product_service.create_product(product)
# Set Location header
response.headers["Location"] = f"{request.url}/{new_product.id}"
return new_product
@app.put(
"/v1/products/{product_id}",
response_model=Product,
summary="Update product",
tags=["Products"]
)
async def update_product(
product_id: str,
product: ProductCreate,
user=Depends(get_current_user)
):
"""Update an existing product (requires authentication)."""
updated = await product_service.update_product(product_id, product)
if not updated:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Product {product_id} not found"
)
return updated
@app.patch(
"/v1/products/{product_id}",
response_model=Product,
summary="Partial update product",
tags=["Products"]
)
async def patch_product(
product_id: str,
product: ProductUpdate,
user=Depends(get_current_user)
):
"""Partially update a product (requires authentication)."""
# Filter out None values
update_data = product.dict(exclude_unset=True)
if not update_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No fields to update"
)
updated = await product_service.patch_product(product_id, update_data)
if not updated:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Product {product_id} not found"
)
return updated
@app.delete(
"/v1/products/{product_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete product",
tags=["Products"]
)
async def delete_product(
product_id: str,
user=Depends(get_current_user)
):
"""Delete a product (requires authentication)."""
deleted = await product_service.delete_product(product_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Product {product_id} not found"
)
return None
# Error handlers
@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
return JSONResponse(
status_code=400,
content={
"code": "VALIDATION_ERROR",
"message": str(exc)
}
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"code": f"HTTP_{exc.status_code}",
"message": exc.detail
}
)
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info"
)
// tests/products.test.js
const request = require('supertest');
const { app } = require('../src/app');
const { generateToken } = require('../src/utils/auth');
describe('Products API', () => {
let authToken;
let productId;
beforeAll(async () => {
// Generate auth token for protected routes
authToken = await generateToken({ userId: 'test-user', role: 'admin' });
});
describe('GET /v1/products', () => {
it('should return paginated products', async () => {
const res = await request(app)
.get('/v1/products')
.query({ page: 1, perPage: 10 })
.expect(200);
expect(res.body).toHaveProperty('data');
expect(res.body).toHaveProperty('pagination');
expect(res.body).toHaveProperty('links');
expect(res.body.data).toBeInstanceOf(Array);
expect(res.body.data.length).toBeLessThanOrEqual(10);
});
it('should filter products by category', async () => {
const res = await request(app)
.get('/v1/products')
.query({ category: 'electronics' })
.expect(200);
res.body.data.forEach(product => {
expect(product.category).toBe('electronics');
});
});
it('should handle invalid query parameters', async () => {
const res = await request(app)
.get('/v1/products')
.query({ page: -1, perPage: 1000 })
.expect(400);
expect(res.body).toHaveProperty('code');
expect(res.body).toHaveProperty('message');
});
});
describe('POST /v1/products', () => {
it('should create a new product', async () => {
const newProduct = {
name: 'Test Product',
description: 'Test description',
price: 99.99,
inventory: 100,
category: 'electronics'
};
const res = await request(app)
.post('/v1/products')
.set('Authorization', `Bearer ${authToken}`)
.send(newProduct)
.expect(201);
expect(res.body).toHaveProperty('id');
expect(res.body.name).toBe(newProduct.name);
expect(res.headers).toHaveProperty('location');
productId = res.body.id;
});
it('should validate required fields', async () => {
const res = await request(app)
.post('/v1/products')
.set('Authorization', `Bearer ${authToken}`)
.send({ name: 'Incomplete' })
.expect(422);
expect(res.body.code).toBe('VALIDATION_ERROR');
});
it('should require authentication', async () => {
await request(app)
.post('/v1/products')
.send({ name: 'Test' })
.expect(401);
});
});
describe('GET /v1/products/:id', () => {
it('should return a single product', async () => {
const res = await request(app)
.get(`/v1/products/${productId}`)
.expect(200);
expect(res.body.id).toBe(productId);
expect(res.body).toHaveProperty('_links');
});
it('should return 404 for non-existent product', async () => {
await request(app)
.get('/v1/products/non-existent')
.expect(404);
});
});
describe('PATCH /v1/products/:id', () => {
it('should partially update a product', async () => {
const updates = { price: 89.99 };
const res = await request(app)
.patch(`/v1/products/${productId}`)
.set('Authorization', `Bearer ${authToken}`)
.send(updates)
.expect(200);
expect(res.body.price).toBe(89.99);
});
});
describe('DELETE /v1/products/:id', () => {
it('should delete a product', async () => {
await request(app)
.delete(`/v1/products/${productId}`)
.set('Authorization', `Bearer ${authToken}`)
.expect(204);
// Verify deletion
await request(app)
.get(`/v1/products/${productId}`)
.expect(404);
});
});
});
| Error | Cause | Solution |
|---|---|---|
| "Invalid request body" | Malformed JSON or missing fields | Validate against schema |
| "Unauthorized" | Missing or invalid auth token | Include valid Bearer token |
| "Rate limit exceeded" | Too many requests | Implement backoff strategy |
| "Resource not found" | Invalid ID or deleted resource | Verify resource exists |
| "Validation error" | Data doesn't match schema | Check field requirements |
Framework Options
Express.js: Node.js, middleware ecosystemFastAPI: Python, automatic OpenAPI docsNestJS: TypeScript, enterprise patternsDjango REST: Python, batteries includedSpring Boot: Java, enterprise featuresDatabase Options
PostgreSQL: Relational, ACID complianceMongoDB: Document store, flexible schemaMySQL: Relational, wide supportDynamoDB: Serverless, auto-scalingDO:
DON'T:
/api-documentation-generator - Generate API docs/api-gateway-builder - Create API gateway/graphql-server-builder - Build GraphQL APIs/grpc-service-generator - Create gRPC services/webhook-handler-creator - Handle webhooks