Skip to main content

Agent Skills Framework Extension (Optional)

API Design Patterns Skill

When to Use This Skill

Use this skill when implementing api design patterns patterns in your codebase.

How to Use This Skill

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

RESTful and GraphQL API design with OpenAPI specifications, versioning, and contract-first development.

Core Capabilities

  1. REST API Design - Resource modeling, HTTP methods, status codes
  2. GraphQL Design - Schema, resolvers, subscriptions
  3. OpenAPI Specification - Contract-first development
  4. Versioning - URL, header, and query parameter strategies
  5. Error Handling - Consistent error responses

OpenAPI Specification

# openapi/api.yaml
openapi: 3.1.0
info:
title: CODITECT API
version: 1.0.0
description: AI-powered development platform API
contact:
name: API Support
email: api@coditect.ai
license:
name: Proprietary
url: https://coditect.ai/terms

servers:
- url: https://api.coditect.ai/v1
description: Production
- url: https://api.staging.coditect.ai/v1
description: Staging

security:
- bearerAuth: []

tags:
- name: Projects
description: Project management endpoints
- name: Agents
description: AI agent operations
- name: Users
description: User management

paths:
/projects:
get:
summary: List projects
operationId: listProjects
tags: [Projects]
parameters:
- $ref: '#/components/parameters/PageSize'
- $ref: '#/components/parameters/PageToken'
- $ref: '#/components/parameters/Filter'
responses:
'200':
description: List of projects
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectList'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'

post:
summary: Create project
operationId: createProject
tags: [Projects]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateProjectRequest'
responses:
'201':
description: Project created
content:
application/json:
schema:
$ref: '#/components/schemas/Project'
headers:
Location:
schema:
type: string
description: URL of created project
'400':
$ref: '#/components/responses/BadRequest'
'409':
$ref: '#/components/responses/Conflict'

/projects/{projectId}:
get:
summary: Get project
operationId: getProject
tags: [Projects]
parameters:
- $ref: '#/components/parameters/ProjectId'
responses:
'200':
description: Project details
content:
application/json:
schema:
$ref: '#/components/schemas/Project'
'404':
$ref: '#/components/responses/NotFound'

patch:
summary: Update project
operationId: updateProject
tags: [Projects]
parameters:
- $ref: '#/components/parameters/ProjectId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateProjectRequest'
responses:
'200':
description: Project updated
content:
application/json:
schema:
$ref: '#/components/schemas/Project'

delete:
summary: Delete project
operationId: deleteProject
tags: [Projects]
parameters:
- $ref: '#/components/parameters/ProjectId'
responses:
'204':
description: Project deleted
'404':
$ref: '#/components/responses/NotFound'

components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT

parameters:
ProjectId:
name: projectId
in: path
required: true
schema:
type: string
format: uuid
description: Project identifier

PageSize:
name: pageSize
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
description: Number of items per page

PageToken:
name: pageToken
in: query
schema:
type: string
description: Token for next page

Filter:
name: filter
in: query
schema:
type: string
description: Filter expression (e.g., status="active")

schemas:
Project:
type: object
required: [id, name, status, createdAt]
properties:
id:
type: string
format: uuid
readOnly: true
name:
type: string
minLength: 3
maxLength: 100
description:
type: string
maxLength: 1000
status:
type: string
enum: [active, archived, deleted]
settings:
$ref: '#/components/schemas/ProjectSettings'
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true

ProjectSettings:
type: object
properties:
visibility:
type: string
enum: [private, team, public]
default: private
agentModel:
type: string
enum: [sonnet, opus, haiku]
default: sonnet

CreateProjectRequest:
type: object
required: [name]
properties:
name:
type: string
minLength: 3
maxLength: 100
description:
type: string
maxLength: 1000
settings:
$ref: '#/components/schemas/ProjectSettings'

UpdateProjectRequest:
type: object
properties:
name:
type: string
minLength: 3
maxLength: 100
description:
type: string
maxLength: 1000
settings:
$ref: '#/components/schemas/ProjectSettings'

ProjectList:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/Project'
nextPageToken:
type: string
totalCount:
type: integer

Error:
type: object
required: [code, message]
properties:
code:
type: string
description: Machine-readable error code
message:
type: string
description: Human-readable error message
details:
type: array
items:
type: object
properties:
field:
type: string
reason:
type: string
requestId:
type: string
description: Request ID for support

responses:
BadRequest:
description: Invalid request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: INVALID_REQUEST
message: Request validation failed
details:
- field: name
reason: Name is required

Unauthorized:
description: Authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: UNAUTHORIZED
message: Authentication required

NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: NOT_FOUND
message: Project not found

Conflict:
description: Resource conflict
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: CONFLICT
message: Project with this name already exists

RateLimited:
description: Rate limit exceeded
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: RATE_LIMITED
message: Rate limit exceeded
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying
X-RateLimit-Limit:
schema:
type: integer
X-RateLimit-Remaining:
schema:
type: integer

GraphQL Schema

# schema.graphql

scalar DateTime
scalar UUID

type Query {
"""Get current user"""
me: User!

"""Get project by ID"""
project(id: UUID!): Project

"""List projects with filtering and pagination"""
projects(
first: Int = 20
after: String
filter: ProjectFilter
): ProjectConnection!

"""Search projects"""
searchProjects(query: String!, first: Int = 10): [Project!]!
}

type Mutation {
"""Create a new project"""
createProject(input: CreateProjectInput!): CreateProjectPayload!

"""Update project settings"""
updateProject(input: UpdateProjectInput!): UpdateProjectPayload!

"""Delete a project"""
deleteProject(id: UUID!): DeleteProjectPayload!

"""Invite member to project"""
inviteMember(input: InviteMemberInput!): InviteMemberPayload!
}

type Subscription {
"""Subscribe to project updates"""
projectUpdated(projectId: UUID!): ProjectUpdatedEvent!

"""Subscribe to agent task progress"""
agentTaskProgress(taskId: UUID!): AgentTaskProgressEvent!
}

type User {
id: UUID!
email: String!
name: String!
avatar: String
projects(first: Int = 10, after: String): ProjectConnection!
createdAt: DateTime!
}

type Project {
id: UUID!
name: String!
description: String
status: ProjectStatus!
settings: ProjectSettings!
members: [ProjectMember!]!
agentTasks(first: Int = 10, after: String): AgentTaskConnection!
createdAt: DateTime!
updatedAt: DateTime!
}

type ProjectSettings {
visibility: Visibility!
agentModel: AgentModel!
autoComplete: Boolean!
}

type ProjectMember {
user: User!
role: MemberRole!
joinedAt: DateTime!
}

type ProjectConnection {
edges: [ProjectEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}

type ProjectEdge {
node: Project!
cursor: String!
}

type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}

enum ProjectStatus {
ACTIVE
ARCHIVED
DELETED
}

enum Visibility {
PRIVATE
TEAM
PUBLIC
}

enum AgentModel {
SONNET
OPUS
HAIKU
}

enum MemberRole {
OWNER
ADMIN
MEMBER
VIEWER
}

input CreateProjectInput {
name: String!
description: String
visibility: Visibility = PRIVATE
agentModel: AgentModel = SONNET
}

input UpdateProjectInput {
id: UUID!
name: String
description: String
settings: ProjectSettingsInput
}

input ProjectSettingsInput {
visibility: Visibility
agentModel: AgentModel
autoComplete: Boolean
}

input ProjectFilter {
status: ProjectStatus
visibility: Visibility
createdAfter: DateTime
createdBefore: DateTime
}

input InviteMemberInput {
projectId: UUID!
email: String!
role: MemberRole!
}

type CreateProjectPayload {
project: Project
errors: [UserError!]!
}

type UpdateProjectPayload {
project: Project
errors: [UserError!]!
}

type DeleteProjectPayload {
deletedId: UUID
errors: [UserError!]!
}

type InviteMemberPayload {
member: ProjectMember
errors: [UserError!]!
}

type UserError {
field: String
message: String!
code: String!
}

type ProjectUpdatedEvent {
project: Project!
updatedFields: [String!]!
updatedBy: User!
}

type AgentTaskProgressEvent {
taskId: UUID!
status: TaskStatus!
progress: Float!
message: String
}

enum TaskStatus {
PENDING
RUNNING
COMPLETED
FAILED
}

Error Handling

// src/api/errors.ts

export class APIError extends Error {
constructor(
public readonly code: string,
message: string,
public readonly status: number = 500,
public readonly details?: ErrorDetail[]
) {
super(message);
this.name = 'APIError';
}

toJSON() {
return {
code: this.code,
message: this.message,
details: this.details,
requestId: this.requestId,
};
}
}

export class ValidationError extends APIError {
constructor(details: ErrorDetail[]) {
super('VALIDATION_ERROR', 'Request validation failed', 400, details);
}
}

export class NotFoundError extends APIError {
constructor(resource: string, id: string) {
super('NOT_FOUND', `${resource} with id ${id} not found`, 404);
}
}

export class UnauthorizedError extends APIError {
constructor(message = 'Authentication required') {
super('UNAUTHORIZED', message, 401);
}
}

export class ForbiddenError extends APIError {
constructor(message = 'Access denied') {
super('FORBIDDEN', message, 403);
}
}

export class ConflictError extends APIError {
constructor(message: string) {
super('CONFLICT', message, 409);
}
}

export class RateLimitError extends APIError {
constructor(
public readonly retryAfter: number,
public readonly limit: number,
public readonly remaining: number
) {
super('RATE_LIMITED', 'Rate limit exceeded', 429);
}
}

// Error middleware
export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
const requestId = req.headers['x-request-id'] || crypto.randomUUID();

if (err instanceof APIError) {
const response = {
...err.toJSON(),
requestId,
};

if (err instanceof RateLimitError) {
res.setHeader('Retry-After', err.retryAfter);
res.setHeader('X-RateLimit-Limit', err.limit);
res.setHeader('X-RateLimit-Remaining', err.remaining);
}

return res.status(err.status).json(response);
}

// Log unexpected errors
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
requestId,
path: req.path,
});

res.status(500).json({
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
requestId,
});
}

Pagination Patterns

// src/api/pagination.ts

interface CursorPagination<T> {
items: T[];
pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
totalCount: number;
}

export async function paginate<T extends { id: string; createdAt: Date }>(
query: QueryBuilder<T>,
options: {
first?: number;
after?: string;
last?: number;
before?: string;
}
): Promise<CursorPagination<T>> {
const { first = 20, after, last, before } = options;
const limit = first || last || 20;

let cursorCondition: WhereCondition | undefined;

if (after) {
const { id, createdAt } = decodeCursor(after);
cursorCondition = {
OR: [
{ createdAt: { $lt: createdAt } },
{ createdAt: createdAt, id: { $gt: id } },
],
};
} else if (before) {
const { id, createdAt } = decodeCursor(before);
cursorCondition = {
OR: [
{ createdAt: { $gt: createdAt } },
{ createdAt: createdAt, id: { $lt: id } },
],
};
}

if (cursorCondition) {
query = query.where(cursorCondition);
}

// Fetch one extra to determine hasNextPage
const items = await query
.orderBy('createdAt', 'DESC')
.orderBy('id', 'ASC')
.limit(limit + 1)
.execute();

const hasMore = items.length > limit;
const nodes = hasMore ? items.slice(0, limit) : items;

const totalCount = await query.count();

return {
items: nodes,
pageInfo: {
hasNextPage: hasMore,
hasPreviousPage: !!after,
startCursor: nodes.length > 0 ? encodeCursor(nodes[0]) : null,
endCursor: nodes.length > 0 ? encodeCursor(nodes[nodes.length - 1]) : null,
},
totalCount,
};
}

function encodeCursor(item: { id: string; createdAt: Date }): string {
return Buffer.from(JSON.stringify({
id: item.id,
createdAt: item.createdAt.toISOString(),
})).toString('base64url');
}

function decodeCursor(cursor: string): { id: string; createdAt: Date } {
const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString());
return {
id: decoded.id,
createdAt: new Date(decoded.createdAt),
};
}

API Versioning

// src/api/versioning.ts

export const API_VERSIONS = ['v1', 'v2'] as const;
export type APIVersion = typeof API_VERSIONS[number];

// Version router
export function createVersionedRouter() {
const router = express.Router();

// Version from URL path
router.use('/v1', v1Router);
router.use('/v2', v2Router);

// Version from header (Accept-Version)
router.use((req, res, next) => {
const version = req.headers['accept-version'] as APIVersion;
if (version && API_VERSIONS.includes(version)) {
req.apiVersion = version;
} else {
req.apiVersion = 'v1'; // Default version
}
next();
});

return router;
}

// Version-aware response transformation
export function transformResponse<T>(data: T, version: APIVersion): unknown {
const transformer = responseTransformers[version];
return transformer ? transformer(data) : data;
}

const responseTransformers: Record<APIVersion, (data: any) => any> = {
v1: (data) => data,
v2: (data) => {
// V2 uses camelCase, V1 uses snake_case
return Object.fromEntries(
Object.entries(data).map(([k, v]) => [toCamelCase(k), v])
);
},
};

Usage Examples

Create OpenAPI Specification

Apply api-design-patterns skill to design RESTful API with OpenAPI 3.1 specification for project management

Design GraphQL Schema

Apply api-design-patterns skill to create GraphQL schema with subscriptions for real-time updates

Implement Pagination

Apply api-design-patterns skill to add cursor-based pagination with consistent ordering

Integration Points

  • system-architecture-design - API contracts between services
  • database-schema-optimization - Query optimization for API endpoints
  • multi-tenant-security - API authentication and authorization
  • monitoring-observability - API metrics and tracing

Success Output

When successful, this skill MUST output:

✅ SKILL COMPLETE: api-design-patterns

Completed:
- [x] API specification created (OpenAPI or GraphQL schema)
- [x] Resource models defined with validation rules
- [x] Error handling standardized across all endpoints
- [x] Authentication/authorization implemented
- [x] Pagination strategy selected and implemented
- [x] API versioning strategy defined
- [x] Rate limiting configured

Outputs:
- openapi/api.yaml or schema.graphql
- Error response schemas
- Pagination implementation
- Authentication middleware
- API documentation

Completion Checklist

Before marking this skill as complete, verify:

  • OpenAPI 3.1+ spec validates without errors (use Swagger validator)
  • All endpoints have complete request/response schemas
  • Error responses include machine-readable codes and human-readable messages
  • Authentication strategy implemented (Bearer, API Key, OAuth2)
  • Rate limiting configured with appropriate headers
  • Pagination implemented (cursor-based preferred for scale)
  • API versioning strategy documented and implemented
  • All schemas include validation rules (min/max, regex, enums)
  • Breaking changes identified and migration path documented

Failure Indicators

This skill has FAILED if:

  • ❌ OpenAPI spec has validation errors or missing required fields
  • ❌ Endpoints return inconsistent error response formats
  • ❌ No authentication/authorization implemented
  • ❌ Pagination uses offset-based approach for large datasets (>100K records)
  • ❌ No API versioning strategy defined
  • ❌ Rate limiting missing or improperly configured
  • ❌ Breaking changes introduced without version bump
  • ❌ Response schemas don't match actual API behavior
  • ❌ Missing security schemes in OpenAPI spec

When NOT to Use

Do NOT use this skill when:

  • Designing internal RPC/gRPC services (use grpc-patterns instead)
  • Creating WebSocket or real-time APIs only (use realtime-api-patterns)
  • Building GraphQL-only APIs without REST fallback (use graphql-patterns directly)
  • Designing event-driven message schemas (use event-driven-patterns)
  • Creating CLI tools or SDK interfaces (use sdk-design-patterns)
  • Prototyping without production requirements (skip comprehensive error handling first)
  • Building single-purpose microservices with 1-2 endpoints (keep it simple)

Anti-Patterns (Avoid)

Anti-PatternProblemSolution
Versioning in query paramsHard to route, inconsistentUse URL path versioning (/v1/, /v2/)
Offset pagination for large datasetsO(n) performance degradationUse cursor-based pagination
Generic error messagesImpossible to debugInclude error codes, field-level details
Missing HTTP status codesConfuses client error handlingUse semantic status codes (400, 401, 404, 409, 429)
No rate limitingService vulnerable to abuseImplement rate limiting with retry headers
Returning full objects on updatesUnnecessary bandwidthReturn updated resource or 204 No Content
Ignoring idempotencyDuplicate requests cause issuesUse idempotency keys for POST/PUT
Breaking changes in same versionClient apps break unexpectedlyCreate new version for breaking changes

Principles

This skill embodies:

  • #1 Recycle → Extend → Re-Use → Create - Reuse OpenAPI schemas and error patterns
  • #2 First Principles - RESTful design principles and HTTP semantics
  • #3 Keep It Simple - Clear, predictable API contracts
  • #5 Eliminate Ambiguity - Explicit schemas, validation rules, error codes
  • #6 Clear, Understandable, Explainable - Self-documenting API specifications
  • #10 Quality Over Speed - Comprehensive API design prevents future breaking changes

Full Principles: CODITECT-STANDARD-AUTOMATION.md