api-design

API Design - REST & GraphQL

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "api-design" with this command: npx skills add dsantiagomj/dsmj-ai-toolkit/dsantiagomj-dsmj-ai-toolkit-api-design

API Design - REST & GraphQL

Design consistent, intuitive APIs that scale

When to Use This Skill

Use this skill when:

  • Designing new API endpoints (REST or GraphQL)

  • Creating HTTP routes and handlers

  • Implementing pagination, filtering, or sorting

  • Versioning APIs for backward compatibility

  • Handling API errors and validation

  • Designing GraphQL schemas and resolvers

  • Optimizing API performance (N+1 queries, caching)

Don't use this skill for:

  • Frontend-only work with no API involvement

  • Direct database queries without an API layer

  • Internal function calls (not exposed as API)

Critical Patterns

Pattern 1: HTTP Methods and Status Codes

When: Building RESTful endpoints

Good:

// Correct HTTP methods and status codes export async function GET(request: Request) { const users = await db.user.findMany(); return NextResponse.json(users, { status: 200 }); }

export async function POST(request: Request) { const body = await request.json();

if (!body.email || !body.name) { return NextResponse.json( { error: 'Email and name are required' }, { status: 400 } // Bad Request ); }

const user = await db.user.create({ data: body }); return NextResponse.json(user, { status: 201 }); // Created }

export async function DELETE(request: Request) { await db.user.delete({ where: { id: '123' } }); return new Response(null, { status: 204 }); // No Content }

Bad:

// ❌ Wrong: Using POST for everything export async function POST(request: Request) { const { action, userId } = await request.json();

if (action === 'get') { const user = await db.user.findUnique({ where: { id: userId } }); return NextResponse.json(user); // Should be GET }

if (action === 'delete') { await db.user.delete({ where: { id: userId } }); return NextResponse.json({ success: true }); // Should be DELETE } }

// ❌ Wrong: Always returning 200 export async function GET(request: Request) { const user = await db.user.findUnique({ where: { id: '999' } }); if (!user) { return NextResponse.json({ error: 'Not found' }, { status: 200 }); // Should be 404 } return NextResponse.json(user); }

Why: Correct HTTP methods and status codes make APIs predictable and RESTful. Clients can rely on standard semantics.

HTTP Methods Quick Reference:

GET - Retrieve (Safe, Idempotent, Cacheable) POST - Create (Not safe, Not idempotent) PUT - Replace entire resource (Not safe, Idempotent) PATCH - Partial update (Not safe, Usually idempotent) DELETE - Remove (Not safe, Idempotent)

Status Codes Quick Reference:

2xx Success: 200 OK - Successful GET, PUT, PATCH, DELETE 201 Created - Successful POST (resource created) 204 No Content - Successful DELETE (no response body)

4xx Client Errors: 400 Bad Request - Invalid request data 401 Unauthorized - Authentication required 403 Forbidden - Authenticated but not authorized 404 Not Found - Resource doesn't exist 409 Conflict - Resource conflict (duplicate) 422 Unprocessable - Validation errors

5xx Server Errors: 500 Internal - Unexpected server error 503 Unavailable - Server temporarily unavailable

Pattern 2: Resource Naming and Nesting

When: Designing API URL structure

Good:

// ✅ Use nouns, not verbs GET /api/users POST /api/users GET /api/users/123 DELETE /api/users/123

// ✅ Plural nouns for collections GET /api/products GET /api/orders

// ✅ Nested resources (max 2 levels) GET /api/users/123/posts POST /api/users/123/posts GET /api/users/123/posts/456

Bad:

// ❌ Wrong: Verbs in URLs GET /api/getUsers POST /api/createUser DELETE /api/deleteUser/123

// ❌ Wrong: Singular for collections GET /api/user GET /api/product

// ❌ Wrong: Too deeply nested (3+ levels) GET /api/users/123/posts/456/comments/789/likes

Why: Nouns represent resources, verbs are implied by HTTP methods. Avoid deep nesting to keep URLs simple and predictable.

Nested Resources Pattern:

GET /api/users/123/posts - Get all posts by user 123 GET /api/users/123/posts/456 - Get specific post by user 123 POST /api/users/123/posts - Create post for user 123

// ⚠️ For deep relationships, use query params instead: GET /api/comments?postId=456 GET /api/likes?commentId=789

Pattern 3: Pagination

When: Returning large collections

Good - Cursor-based (recommended for large datasets):

export async function GET(request: Request) { const { searchParams } = new URL(request.url); const cursor = searchParams.get('cursor'); const limit = parseInt(searchParams.get('limit') || '20');

const users = await db.user.findMany({ take: limit + 1, ...(cursor && { cursor: { id: cursor }, skip: 1 }), orderBy: { id: 'asc' }, });

const hasMore = users.length > limit; const data = hasMore ? users.slice(0, -1) : users;

return NextResponse.json({ data, pagination: { nextCursor: hasMore ? data[data.length - 1].id : null, hasMore, }, }); }

Good - Offset-based (simpler, for admin panels):

export async function GET(request: Request) { const { searchParams } = new URL(request.url); const page = parseInt(searchParams.get('page') || '1'); const limit = parseInt(searchParams.get('limit') || '20');

const [users, total] = await Promise.all([ db.user.findMany({ skip: (page - 1) * limit, take: limit, }), db.user.count(), ]);

return NextResponse.json({ data: users, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }); }

Bad:

// ❌ No pagination - returns all records export async function GET() { const users = await db.user.findMany(); // Could be millions! return NextResponse.json(users); }

Why: Pagination prevents performance issues and timeouts. Cursor-based is more efficient for large datasets, offset-based is simpler for small datasets.

Pattern 4: API Versioning

When: Making breaking changes to existing APIs

Good - URL versioning (most explicit):

GET /api/v1/users GET /api/v2/users

// app/api/v1/users/route.ts export async function GET() { const users = await db.user.findMany(); return NextResponse.json(users); // Old format }

// app/api/v2/users/route.ts export async function GET() { const users = await db.user.findMany({ include: { profile: true }, // New: Include related data }); return NextResponse.json(users); }

Breaking vs Non-Breaking Changes:

// ✅ Non-breaking (no version bump needed): // - Add new endpoint // - Add optional field to request // - Add new field to response

// ❌ Breaking (requires new version): // - Remove endpoint // - Remove field from response // - Rename field // - Change field type // - Make optional field required

Why: Versioning allows backward compatibility while evolving the API. Existing clients continue working while new clients use improved versions.

Pattern 5: Consistent Error Handling

When: Handling errors and validation

Good:

interface ApiError { code: string; message: string; details?: Array<{ field: string; message: string }>; }

function errorResponse(status: number, error: ApiError) { return NextResponse.json({ error }, { status }); }

export async function POST(request: Request) { try { const body = await request.json();

const errors = validateUser(body);
if (errors.length > 0) {
  return errorResponse(400, {
    code: 'VALIDATION_ERROR',
    message: 'Invalid input data',
    details: errors,
  });
}

const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });

} catch (error) { if (error.code === 'P2002') { return errorResponse(409, { code: 'DUPLICATE_EMAIL', message: 'Email already exists', }); }

return errorResponse(500, {
  code: 'INTERNAL_ERROR',
  message: 'An unexpected error occurred',
});

} }

Bad:

// ❌ Inconsistent error formats export async function POST(request: Request) { try { const body = await request.json();

if (!body.email) {
  return NextResponse.json('Email required');  // Plain string
}

const user = await db.user.create({ data: body });
return NextResponse.json(user);

} catch (error) { return NextResponse.json({ message: error.message, // Inconsistent format stack: error.stack, // Leaks implementation details }); } }

Why: Consistent error format makes client error handling predictable. Never expose stack traces or internal details in production.

GraphQL Critical Patterns

Pattern 1: Schema Design with Types and Relationships

Good:

type User { id: ID! name: String! email: String! role: UserRole! posts: [Post!]! createdAt: DateTime! }

enum UserRole { ADMIN USER GUEST }

type Post { id: ID! title: String! content: String! author: User! published: Boolean! }

type Query { user(id: ID!): User users(limit: Int, cursor: String): UserConnection! }

type Mutation { createUser(input: CreateUserInput!): User! updateUser(id: ID!, input: UpdateUserInput!): User! }

input CreateUserInput { name: String! email: String! role: UserRole }

Why: Clear types, non-null fields (the ! suffix), and input types make the API self-documenting and type-safe.

Pattern 2: Solving N+1 Queries with DataLoader

Problem:

// ❌ N+1 queries: 1 for users + N for posts const resolvers = { User: { posts: async (parent, _args, context) => { return context.db.post.findMany({ where: { authorId: parent.id }, }); }, }, }; // Querying 100 users = 101 database queries!

Solution:

// ✅ DataLoader batches queries import DataLoader from 'dataloader';

const postLoader = new DataLoader(async (userIds: readonly string[]) => { const posts = await db.post.findMany({ where: { authorId: { in: [...userIds] } }, });

const postsByUserId = userIds.map(userId => posts.filter(post => post.authorId === userId) );

return postsByUserId; });

const resolvers = { User: { posts: (parent, _args, context) => { return context.loaders.post.load(parent.id); }, }, }; // Querying 100 users = 2 queries (users + batched posts)!

Why: DataLoader batches and caches database queries, solving the N+1 problem and dramatically improving performance.

Anti-Patterns

❌ Anti-Pattern 1: Exposing Database Structure Directly

Don't do this:

// ❌ API mirrors database exactly GET /api/users Response: { id: 123, password_hash: "bcrypt...", // Exposing sensitive data! created_at: "2024-01-15", internal_notes: "VIP customer" }

Do this instead:

// ✅ API has its own contract GET /api/users/123 Response: { id: 123, name: "Alice", email: "alice@example.com", role: "admin", joinedAt: "2024-01-15T10:00:00Z" }

// Server-side: Transform before sending export async function GET(request: Request, { params }) { const user = await db.user.findUnique({ where: { id: params.id } });

return NextResponse.json({ id: user.id, name: user.name, email: user.email, role: user.role, joinedAt: user.createdAt, }); }

❌ Anti-Pattern 2: No Input Validation

Don't do this:

// ❌ Trusting all input export async function POST(request: Request) { const body = await request.json(); const user = await db.user.create({ data: body }); return NextResponse.json(user); }

Do this instead:

// ✅ Validate all input import { z } from 'zod';

const userSchema = z.object({ name: z.string().min(2).max(100), email: z.string().email(), age: z.number().int().min(18).max(120), });

export async function POST(request: Request) { const body = await request.json();

const result = userSchema.safeParse(body); if (!result.success) { return NextResponse.json( { error: result.error.format() }, { status: 400 } ); }

const user = await db.user.create({ data: result.data }); return NextResponse.json(user, { status: 201 }); }

❌ Anti-Pattern 3: Ignoring Authentication

Don't do this:

// ❌ No auth checks export async function DELETE(request: Request) { const { id } = await request.json(); await db.user.delete({ where: { id } }); return NextResponse.json({ success: true }); }

Do this instead:

// ✅ Check authentication and authorization export async function DELETE(request: Request) { const session = await getSession(request);

if (!session) { return new Response('Unauthorized', { status: 401 }); }

if (session.role !== 'ADMIN') { return new Response('Forbidden', { status: 403 }); }

const { id } = await request.json(); await db.user.delete({ where: { id } }); return new Response(null, { status: 204 }); }

Code Examples

Example 1: RESTful CRUD Endpoint

// app/api/users/[id]/route.ts export async function GET(request: Request, { params }: { params: { id: string } }) { const user = await db.user.findUnique({ where: { id: params.id } });

if (!user) { return new Response('User not found', { status: 404 }); }

return NextResponse.json(user); }

export async function PATCH(request: Request, { params }: { params: { id: string } }) { const body = await request.json(); const user = await db.user.update({ where: { id: params.id }, data: body, });

return NextResponse.json(user); }

Example 2: GraphQL Resolver with DataLoader

const resolvers = { Query: { users: async (_parent, { limit = 20, cursor }, context) => { const users = await context.db.user.findMany({ take: limit + 1, ...(cursor && { cursor: { id: cursor }, skip: 1 }), });

  const hasMore = users.length > limit;
  const data = hasMore ? users.slice(0, -1) : users;

  return {
    edges: data.map(user => ({ node: user, cursor: user.id })),
    pageInfo: {
      hasNextPage: hasMore,
      endCursor: hasMore ? data[data.length - 1].id : null,
    },
  };
},

}, User: { posts: (parent, _args, context) => { return context.loaders.posts.load(parent.id); }, }, };

For comprehensive examples and detailed implementations, see the references/ folder.

Quick Reference

REST Checklist

  • Use appropriate HTTP methods (GET, POST, PUT, PATCH, DELETE)

  • Return correct status codes (2xx, 4xx, 5xx)

  • Use nouns for resources, not verbs

  • Implement pagination for collections

  • Version API for breaking changes

  • Validate all input

  • Return consistent error format

  • Add authentication/authorization checks

GraphQL Checklist

  • Design clear schema with types and relationships

  • Use DataLoader to prevent N+1 queries

  • Validate inputs with Zod or similar

  • Return meaningful error codes in extensions

  • Implement authentication via context

  • Use connection pattern for pagination

  • Keep mutations simple and focused

Progressive Disclosure

For detailed implementations, see:

  • REST Patterns - Pagination, filtering, versioning, rate limiting, HATEOAS

  • GraphQL Design - Resolvers, DataLoader, subscriptions, directives, input validation

References

  • REST Patterns Reference

  • GraphQL Design Reference

  • GraphQL Documentation

  • REST API Tutorial

Maintained by dsmj-ai-toolkit

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

zustand

No summary provided by upstream source.

Repository SourceNeeds Review
General

performance

No summary provided by upstream source.

Repository SourceNeeds Review
General

vercel-ai-sdk

No summary provided by upstream source.

Repository SourceNeeds Review