Skip to main content

Building APIs with Cursor: REST, GraphQL, and FastAPI

Cursor API Development

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);
expect(response.body.data.email).toBe('[email protected]');
});

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​

PracticeImplementation
VersioningUse /api/v1/ prefix in URLs
PaginationReturn { data, meta: { page, limit, total } }
FilteringUse query params: ?status=active&sort=created_at
Error FormatConsistent { success: false, error: "message" }
Rate Limiting100 requests per 15 minutes for public endpoints
CachingUse 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​

TaskCursor 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"

Last updated: July 2025