rest-api-design

This skill helps you design and implement REST API endpoints following project patterns with Zod validation and OpenAPI documentation.

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 "rest-api-design" with this command: npx skills add curiositech/some_claude_skills/curiositech-some-claude-skills-rest-api-design

REST API Design

This skill helps you design and implement REST API endpoints following project patterns with Zod validation and OpenAPI documentation.

When to Use

✅ USE this skill for:

  • Creating new REST API endpoints with Next.js App Router

  • Designing request/response schemas with Zod

  • Implementing proper error handling and status codes

  • Adding rate limiting and authentication

  • Generating OpenAPI documentation

❌ DO NOT use for:

  • GraphQL APIs → different paradigm entirely

  • Cloudflare Workers → use cloudflare-worker-dev skill

  • Supabase Edge Functions → use Supabase docs

  • WebSocket/real-time APIs → different patterns

API Route Structure

src/app/api/ ├── auth/ # Authentication endpoints ├── check-in/ # Daily check-in CRUD ├── chat/ # AI coaching chat ├── journal/ # Journal entries ├── admin/ # Admin-only endpoints └── health/ # Health check

Standard Route Template

// src/app/api/[feature]/route.ts import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; import { getSession } from '@/lib/auth'; import { createRateLimiter } from '@/lib/rate-limit'; import { logPHIAccess } from '@/lib/hipaa/audit'; import { db } from '@/db';

// 1. Define schemas const RequestSchema = z.object({ field: z.string().min(1).max(1000), optional: z.string().optional(), enumField: z.enum(['option1', 'option2']), number: z.number().int().positive(), });

const ResponseSchema = z.object({ id: z.string(), createdAt: z.string().datetime(), });

// 2. Configure rate limiter const rateLimiter = createRateLimiter({ windowMs: 60000, // 1 minute maxRequests: 30, // 30 requests per window keyPrefix: 'api:feature', });

// 3. Implement handlers export async function GET(request: NextRequest) { // Auth check const session = await getSession(); if (!session) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); }

// Rate limit const rateLimitResult = await rateLimiter.check(session.userId); if (!rateLimitResult.allowed) { return NextResponse.json( { error: 'Rate limit exceeded' }, { status: 429, headers: rateLimitResult.headers } ); }

// Query data const data = await db.query.features.findMany({ where: eq(features.userId, session.userId), });

// Audit log (if PHI) await logPHIAccess(session.userId, 'feature', null, 'LIST');

return NextResponse.json(data); }

export async function POST(request: NextRequest) { // Auth check const session = await getSession(); if (!session) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); }

// Rate limit const rateLimitResult = await rateLimiter.check(session.userId); if (!rateLimitResult.allowed) { return NextResponse.json( { error: 'Rate limit exceeded' }, { status: 429, headers: rateLimitResult.headers } ); }

// Parse and validate body let body: unknown; try { body = await request.json(); } catch { return NextResponse.json( { error: 'Invalid JSON' }, { status: 400 } ); }

const parsed = RequestSchema.safeParse(body); if (!parsed.success) { return NextResponse.json( { error: 'Validation failed', details: parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message, })), }, { status: 400 } ); }

// Create resource const [created] = await db.insert(features).values({ id: generateId(), userId: session.userId, ...parsed.data, createdAt: new Date(), }).returning();

// Audit log await logPHIAccess(session.userId, 'feature', created.id, 'CREATE');

return NextResponse.json(created, { status: 201 }); }

Zod Schema Patterns

Basic Types

import { z } from 'zod';

const Schema = z.object({ // Strings name: z.string().min(1).max(100), email: z.string().email(), url: z.string().url(), uuid: z.string().uuid(),

// Numbers count: z.number().int().positive(), rating: z.number().min(1).max(5), price: z.number().nonnegative(),

// Booleans isActive: z.boolean(),

// Dates date: z.string().datetime(), dateOnly: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),

// Enums status: z.enum(['pending', 'approved', 'denied']),

// Arrays tags: z.array(z.string()).min(1).max(10),

// Optional fields notes: z.string().optional(), metadata: z.record(z.string()).optional(),

// Nullable deletedAt: z.string().datetime().nullable(), });

Advanced Patterns

// Discriminated unions const EventSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('click'), x: z.number(), y: z.number() }), z.object({ type: z.literal('keypress'), key: z.string() }), ]);

// Refinements const PasswordSchema = z.string() .min(12, 'Password must be at least 12 characters') .regex(/[A-Z]/, 'Must contain uppercase') .regex(/[a-z]/, 'Must contain lowercase') .regex(/[0-9]/, 'Must contain number') .regex(/[^A-Za-z0-9]/, 'Must contain special character');

// Transform const DateSchema = z.string() .datetime() .transform(str => new Date(str));

// Preprocess (coerce types) const NumberFromString = z.preprocess( val => typeof val === 'string' ? parseInt(val, 10) : val, z.number() );

Query Parameter Validation

export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url);

const QuerySchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().min(1).max(100).default(20), sort: z.enum(['asc', 'desc']).default('desc'), status: z.enum(['all', 'active', 'archived']).optional(), });

const query = QuerySchema.safeParse({ page: searchParams.get('page'), limit: searchParams.get('limit'), sort: searchParams.get('sort'), status: searchParams.get('status'), });

if (!query.success) { return NextResponse.json( { error: 'Invalid query parameters', details: query.error.issues }, { status: 400 } ); }

const { page, limit, sort, status } = query.data; // Use validated params... }

Error Response Format

// Standard error response interface APIError { error: string; // Human-readable message code?: string; // Machine-readable code details?: ErrorDetail[]; // Validation details }

interface ErrorDetail { path: string; message: string; }

// Error responses return NextResponse.json( { error: 'Not found', code: 'NOT_FOUND' }, { status: 404 } );

return NextResponse.json( { error: 'Validation failed', code: 'VALIDATION_ERROR', details: [ { path: 'email', message: 'Invalid email format' }, ], }, { status: 400 } );

HTTP Status Codes

Code Use Case

200 Successful GET, PUT, PATCH

201 Successful POST (created)

204 Successful DELETE (no content)

400 Invalid request/validation error

401 Not authenticated

403 Not authorized (authenticated but forbidden)

404 Resource not found

409 Conflict (duplicate, etc.)

429 Rate limit exceeded

500 Server error

OpenAPI Documentation

Update docs/openapi.yaml when adding endpoints:

paths: /api/feature: get: summary: List features tags: [Features] security: - cookieAuth: [] parameters: - name: page in: query schema: type: integer default: 1 - name: limit in: query schema: type: integer default: 20 maximum: 100 responses: '200': description: Success content: application/json: schema: type: array items: $ref: '#/components/schemas/Feature' '401': $ref: '#/components/responses/Unauthorized'

post:
  summary: Create feature
  tags: [Features]
  security:
    - cookieAuth: []
  requestBody:
    required: true
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/CreateFeatureRequest'
  responses:
    '201':
      description: Created
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Feature'
    '400':
      $ref: '#/components/responses/ValidationError'
    '401':
      $ref: '#/components/responses/Unauthorized'

components: schemas: Feature: type: object properties: id: type: string format: uuid name: type: string createdAt: type: string format: date-time required: [id, name, createdAt]

CreateFeatureRequest:
  type: object
  properties:
    name:
      type: string
      minLength: 1
      maxLength: 100
  required: [name]

responses: Unauthorized: description: Not authenticated content: application/json: schema: type: object properties: error: type: string example: Unauthorized

ValidationError:
  description: Validation failed
  content:
    application/json:
      schema:
        type: object
        properties:
          error:
            type: string
          details:
            type: array
            items:
              type: object
              properties:
                path:
                  type: string
                message:
                  type: string

Route Handler Patterns

Dynamic Routes

// src/app/api/feature/[id]/route.ts export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params;

// Validate ID format if (!isValidUUID(id)) { return NextResponse.json( { error: 'Invalid ID format' }, { status: 400 } ); }

const item = await db.query.features.findFirst({ where: eq(features.id, id), });

if (!item) { return NextResponse.json( { error: 'Not found' }, { status: 404 } ); }

return NextResponse.json(item); }

Pagination

interface PaginatedResponse<T> { data: T[]; pagination: { page: number; limit: number; total: number; totalPages: number; }; }

async function getPaginated(page: number, limit: number) { const offset = (page - 1) * limit;

const [data, [{ count }]] = await Promise.all([ db.query.features.findMany({ limit, offset, orderBy: desc(features.createdAt), }), db.select({ count: count() }).from(features), ]);

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

References

  • Zod Documentation

  • Next.js Route Handlers

  • OpenAPI Specification

  • Dub.co Zod Validation

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

video-processing-editing

No summary provided by upstream source.

Repository SourceNeeds Review
General

interior-design-expert

No summary provided by upstream source.

Repository SourceNeeds Review
General

project-management-guru-adhd

No summary provided by upstream source.

Repository SourceNeeds Review
General

email-composer

No summary provided by upstream source.

Repository SourceNeeds Review