Building APIs with Cursor: REST, GraphQL, and FastAPI

Building robust APIs requires careful planning, consistent patterns, and thorough testing. Cursor's AI capabilities can accelerate every phase of API development, from scaffolding endpoints to generating OpenAPI documentation. This guide covers REST, GraphQL, and FastAPI workflows.
Prerequisites
Before starting:
- Cursor installed with Pro plan (recommended)
- Node.js or Python installed
- Familiarity with HTTP concepts and JSON
REST API Development
Project Scaffolding
Ask Cursor to scaffold a complete REST API:
Create a REST API using Express.js with:
- TypeScript
- Route handlers in src/routes/
- Middleware for logging and error handling
- Input validation with Zod
- JWT authentication
- Rate limiting
- Swagger documentation
Express.js Route Structure
// src/routes/users.ts
import { Router } from 'express';
import { z } from 'zod';
import { authenticate } from '../middleware/auth';
import { validate } from '../middleware/validate';
const router = Router();
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
password: z.string().min(8),
});
router.post(
'/',
authenticate,
validate(createUserSchema),
async (req, res, next) => {
try {
const user = await createUser(req.body);
res.status(201).json({
success: true,
data: user,
});
} catch (error) {
next(error);
}
}
);
router.get('/:id', authenticate, async (req, res, next) => {
try {
const user = await getUserById(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found',
});
}
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
});
export default router;
Unified Error Handling
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
export class AppError extends Error {
constructor(
public statusCode: number,
public message: string,
public isOperational = true
) {
super(message);
Object.setPrototypeOf(this, AppError.prototype);
}
}
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
error: err.message,
});
}
console.error('Unexpected error:', err);
res.status(500).json({
success: false,
error: 'Internal server error',
});
};
FastAPI Development with Cursor
Creating FastAPI Endpoints
# main.py
from fastapi import FastAPI, HTTPException, Depends, status
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel, EmailStr
from typing import List, Optional
import uvicorn
app = FastAPI(
title="My API",
description="A production-ready API built with FastAPI",
version="1.0.0",
)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Pydantic models
class UserCreate(BaseModel):
email: EmailStr
name: str
password: str
class UserResponse(BaseModel):
id: int
email: str
name: str
class Config:
from_attributes = True
# In-memory storage (replace with database)
users_db = {}
user_counter = 0
@app.post(
"/users",
response_model=UserResponse,
status_code=status.HTTP_201_CREATED,
tags=["users"],
)
async def create_user(user: UserCreate):
global user_counter
user_counter += 1
new_user = {
"id": user_counter,
"email": user.email,
"name": user.name,
}
users_db[user_counter] = new_user
return new_user
@app.get(
"/users/{user_id}",
response_model=UserResponse,
tags=["users"],
)
async def get_user(user_id: int):
if user_id not in users_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
return users_db[user_id]
@app.get("/users", response_model=List[UserResponse], tags=["users"])
async def list_users(
skip: int = 0,
limit: int = 100,
):
return list(users_db.values())[skip : skip + limit]
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Dependency Injection
# dependencies.py
from fastapi import Depends, HTTPException, status
from sqlalchemy.orm import Session
from database import SessionLocal
async def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = verify_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication",
)
return user
# Usage
@app.get("/items")
async def read_items(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
return db.query(Item).filter(Item.owner_id == current_user.id).all()
GraphQL API Development
Apollo Server Setup
// src/schema.ts
import { gql } from 'apollo-server-express';
export const typeDefs = gql`
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
}
type Mutation {
createUser(email: String!, name: String!, password: String!): User!
createPost(title: String!, content: String!, authorId: ID!): Post!
}
`;
Resolvers with Cursor
// src/resolvers.ts
export const resolvers = {
Query: {
users: async (_, __, { dataSources }) => {
return dataSources.userAPI.getAllUsers();
},
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUserById(id);
},
},
User: {
posts: async (parent, _, { dataSources }) => {
return dataSources.postAPI.getPostsByAuthor(parent.id);
},
},
Mutation: {
createUser: async (_, args, { dataSources }) => {
return dataSources.userAPI.createUser(args);
},
},
};
GraphQL Schema Design
Use Cursor to generate GraphQL schemas from your database schema. Paste your Prisma schema and ask for the corresponding GraphQL type definitions.
Authentication and Authorization
JWT Implementation
// src/utils/jwt.ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
const JWT_SECRET = process.env.JWT_SECRET!;
const SALT_ROUNDS = 12;
export const hashPassword = async (password: string): Promise<string> => {
return bcrypt.hash(password, SALT_ROUNDS);
};
export const comparePassword = async (
password: string,
hash: string
): Promise<boolean> => {
return bcrypt.compare(password, hash);
};
export const generateToken = (payload: object): string => {
return jwt.sign(payload, JWT_SECRET, { expiresIn: '24h' });
};
export const verifyToken = (token: string): any => {
return jwt.verify(token, JWT_SECRET);
};
Middleware Pattern
// src/middleware/rateLimit.ts
import rateLimit from 'express-rate-limit';
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: {
success: false,
error: 'Too many requests, please try again later',
},
standardHeaders: true,
legacyHeaders: false,
});
API Documentation
OpenAPI with FastAPI
FastAPI automatically generates OpenAPI docs. Access them at:
- Swagger UI:
/docs - ReDoc:
/redoc
Express.js with Swagger
// src/config/swagger.ts
import swaggerJsdoc from 'swagger-jsdoc';
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'API documentation',
},
servers: [
{
url: 'http://localhost:3000/api',
},
],
},
apis: ['./src/routes/*.ts'],
};
export const specs = swaggerJsdoc(options);
Testing APIs
Integration Test Example
// tests/users.test.ts
import request from 'supertest';
import app from '../src/app';
describe('Users API', () => {
describe('POST /users', () => {
it('should create a new user', async () => {
const response = await request(app)
.post('/users')
.send({
name: 'Test User',
password: 'securePassword123',
})
.expect(201);
expect(response.body.success).toBe(true);
});
it('should reject invalid email', async () => {
await request(app)
.post('/users')
.send({
email: 'invalid-email',
name: 'Test',
password: 'password',
})
.expect(400);
});
});
});
API Design Best Practices
| Practice | Implementation |
|---|---|
| Versioning | Use /api/v1/ prefix in URLs |
| Pagination | Return { data, meta: { page, limit, total } } |
| Filtering | Use query params: ?status=active&sort=created_at |
| Error Format | Consistent { success: false, error: "message" } |
| Rate Limiting | 100 requests per 15 minutes for public endpoints |
| Caching | Use ETag headers and Redis for response caching |
Common API Patterns
Repository Pattern
// src/repositories/UserRepository.ts
export class UserRepository {
async findById(id: string): Promise<User | null> {
return prisma.user.findUnique({ where: { id } });
}
async findByEmail(email: string): Promise<User | null> {
return prisma.user.findUnique({ where: { email } });
}
async create(data: CreateUserInput): Promise<User> {
return prisma.user.create({ data });
}
async update(id: string, data: UpdateUserInput): Promise<User> {
return prisma.user.update({ where: { id }, data });
}
async delete(id: string): Promise<void> {
await prisma.user.delete({ where: { id } });
}
}
Environment Configuration
# .env
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
JWT_SECRET=your-secret-key
REDIS_URL=redis://localhost:6379
RATE_LIMIT_MAX=100
Quick Reference
| Task | Cursor Prompt |
|---|---|
| Generate CRUD endpoints | "Create CRUD endpoints for a Product resource with validation" |
| Add authentication | "Add JWT authentication middleware to all protected routes" |
| Create GraphQL schema | "Generate GraphQL types from this Prisma schema" |
| Write tests | "Write integration tests for the user registration endpoint" |
| Add rate limiting | "Add rate limiting to the API with different tiers for auth vs public" |
Related Resources
Last updated: July 2025