AI Coding Rules for API Design and REST
Encode REST resource naming, error response formats, pagination, auth patterns, versioning, and OpenAPI conventions as AI coding rules your team shares.
AI Coding Rules for API Design and REST
Every backend team has API conventions. Resource naming patterns, error response shapes, pagination formats, authentication flows, versioning strategies. These conventions exist because someone made a deliberate decision at some point, and they work. The problem is that AI coding assistants don't know about any of them unless you write them down.
Without explicit rules, AI tools will generate a different error response format every time you ask for a new endpoint. One route returns { message: "Not found" }, another returns { error: { code: 404, detail: "Resource missing" } }, and a third returns a bare string. Your frontend developers end up writing three different error handling paths for what should be one consistent pattern.
This guide covers the specific API design patterns worth encoding as AI rules: REST resource naming, error responses, pagination, authentication, versioning, and OpenAPI documentation. Each section includes rules you can copy and adapt for your own codebase.
If you're new to writing AI rules, start with 10 best practices for AI coding rules first, then come back here for API-specific patterns.
REST resource naming
Resource naming is where consistency matters most and where AI tools drift fastest. Without rules, you'll get a mix of singular and plural nouns, nested and flat routes, and inconsistent use of path parameters.
## REST resource naming
- Use plural nouns for collections: /users, /orders, /products
- Use singular path parameters for individual resources: /users/:id, /orders/:orderId
- Nest resources only one level deep: /users/:id/orders (not /users/:id/orders/:orderId/items/:itemId)
- For deeply nested resources, promote them to top-level: /order-items/:id
- Use kebab-case for multi-word paths: /order-items, /user-profiles (not /orderItems or /order_items)
- Use query parameters for filtering, sorting, and pagination: /users?role=admin&sort=created_at
- Do NOT use verbs in URLs: /users/:id (not /getUser/:id or /deleteUser/:id)
- Do NOT use /api prefix if the service only serves API traffic
The "one level of nesting" rule is worth calling out specifically. AI tools love deep nesting because it feels logically complete: if an order belongs to a user and an item belongs to an order, then /users/:userId/orders/:orderId/items/:itemId seems right. In practice, deep nesting creates brittle URLs, complicates routing logic, and forces the client to know the full ancestry of every resource. Flat is better.
Pair the naming rules with HTTP method conventions:
## HTTP methods
- GET: Read a resource or list resources. Never mutate state.
- POST: Create a new resource. Return 201 with the created resource.
- PUT: Full replacement of a resource. Client sends the complete object.
- PATCH: Partial update. Client sends only the fields to change.
- DELETE: Remove a resource. Return 204 with no body on success.
Do NOT use POST for operations that are idempotent. If the client can safely retry without side effects, use PUT. Note that PATCH is not inherently idempotent per RFC 5789, so design PATCH handlers carefully if retry safety matters.
Do NOT use GET with a request body.
These rules seem obvious, but AI assistants will regularly generate POST endpoints for operations that should be PUT, or return 200 with a body on DELETE. Making the mapping explicit prevents that drift.
Error response format
Error responses are the single most inconsistent thing in AI-generated API code. Every endpoint gets a slightly different shape unless you lock it down.
## Error responses
Every error response uses this exact shape:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable description of what went wrong",
"details": [] // optional array of field-level errors
}
}
Error codes are SCREAMING_SNAKE_CASE strings, not HTTP status codes.
Standard error codes:
- VALIDATION_ERROR (400) - request body or params failed validation
- UNAUTHORIZED (401) - missing or invalid authentication
- FORBIDDEN (403) - authenticated but insufficient permissions
- NOT_FOUND (404) - resource does not exist
- CONFLICT (409) - resource already exists or state conflict
- RATE_LIMITED (429) - too many requests
- INTERNAL_ERROR (500) - unexpected server failure
For validation errors, include field-level details:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be a positive integer" }
]
}
}
Never return raw exception messages to the client.
Never return a bare string as an error.
Always include both the machine-readable code and human-readable message.
The key decision here is the shape. Once your team picks a shape, every endpoint must use it. The rule above uses a nested error object with a code and message, which is a common pattern. Your team might prefer a flatter structure. The format matters less than the consistency.
One thing to encode explicitly: the difference between HTTP status codes and application error codes. AI tools frequently conflate the two, returning "code": 404 as a number inside the error body. Using string error codes like NOT_FOUND separates the transport layer (HTTP status) from the application layer (error code) and gives you room to be more specific. A 400 status could mean VALIDATION_ERROR, MISSING_REQUIRED_FIELD, or INVALID_FORMAT, and the client can branch on the string code without parsing status codes.
Pagination
AI tools default to offset-based pagination, which is fine for many use cases but breaks down at scale. Your rules should specify which strategy to use and what the response shape looks like.
## Pagination
Use cursor-based pagination for all list endpoints.
Request format:
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ
Response format:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTQzfQ",
"has_more": true
}
}
Rules:
- Default limit is 20, maximum is 100
- Cursor is an opaque base64-encoded string (clients must not parse it)
- When has_more is false, next_cursor is null
- First request omits the cursor parameter
- Sort order is fixed per endpoint (document it in OpenAPI spec)
- Do NOT use offset/page-number pagination (unstable with concurrent writes)
If your use case requires offset pagination (admin dashboards, for example, where users jump to "page 47"), you can add that as a separate rule for specific endpoints. But the default should be cursor-based, and the AI should not choose offset pagination unless explicitly told to.
The response shape matters here too. A common AI mistake is to return pagination metadata at the top level mixed with other fields:
// What the AI generates without rules
{ "users": [...], "total": 500, "page": 3, "per_page": 20 }
// What your rule should enforce
{ "data": [...], "pagination": { "next_cursor": "...", "has_more": true } }
The second format is consistent across all endpoints. The first format uses a different key (users, orders, products) for every resource, which means the client needs endpoint-specific parsing logic.
Authentication and authorization patterns
Auth is security-critical, so rules here need to be precise. Ambiguity leads to vulnerabilities.
## Authentication
- All endpoints require authentication unless explicitly marked as public
- Use Bearer tokens in the Authorization header: Authorization: Bearer <token>
- Do NOT accept tokens in query parameters or cookies for API routes
- Validate the token on every request (no caching of auth state in-memory)
- Return 401 for missing/invalid tokens, 403 for insufficient permissions
## Authorization
- Use role-based access control (RBAC) with these roles: admin, member, viewer
- Check permissions at the route handler level, not in business logic
- Use a middleware or decorator pattern for common auth checks:
// Example: require admin role
export async function DELETE(request: NextRequest) {
const user = await requireAuth(request);
requireRole(user, "admin");
// ... rest of handler
}
- Never trust client-provided user IDs for ownership checks. Always derive the user from the token.
- Log all authorization failures with the user ID, requested resource, and required permission.
The "check permissions at the route handler level" rule is important. Without it, AI tools will scatter authorization checks throughout the business logic layer, making it difficult to audit who can access what. Keeping auth at the edge of each handler makes the security model visible and auditable.
Pair this with rules for specific sensitive operations:
## Sensitive operations
- Deletion of resources requires soft-delete (set deleted_at timestamp, don't remove the row)
- Bulk operations (batch delete, bulk update) require admin role
- Rate limit auth endpoints: max 10 login attempts per IP per 5-minute window
- All password/token operations must be constant-time to prevent timing attacks
API versioning
Versioning decisions should be made once and documented as rules. Otherwise, the AI will mix strategies: sometimes adding /v1/ to the path, sometimes using an Accept header, sometimes not versioning at all.
## API versioning
- Use URL path versioning: /v1/users, /v2/users
- The current version is v1
- All routes must include the version prefix
- When introducing breaking changes:
1. Create the new version (v2) with the updated behavior
2. Keep v1 running with the old behavior
3. Mark v1 as deprecated in the OpenAPI spec
4. Remove v1 after all clients have migrated (minimum 6 months)
Breaking changes include:
- Removing a field from a response
- Changing a field's type
- Renaming a field
- Changing the error response shape
- Making a previously optional field required
Non-breaking changes (do NOT require a new version):
- Adding a new optional field to a response
- Adding a new endpoint
- Adding a new optional query parameter
- Relaxing a validation constraint
The list of what counts as "breaking" versus "non-breaking" is the most valuable part of this rule. AI tools don't inherently understand API compatibility. They'll happily rename a response field and call it a minor change. Spelling out the boundary prevents subtle client breakages.
OpenAPI and documentation
If your team uses OpenAPI (Swagger) specs, your rules should enforce consistent documentation alongside the implementation.
## OpenAPI documentation
- Every endpoint must have a corresponding OpenAPI operation in the spec
- Include request body schemas with examples for POST/PUT/PATCH endpoints
- Include response schemas for all status codes (200, 201, 400, 401, 403, 404, 500)
- Use $ref for shared schemas (ErrorResponse, PaginatedResponse, etc.)
- Tag endpoints by resource: users, orders, products
- Include a brief summary (one line) and description (detailed) for every operation
- Keep the spec in sync: when you add or modify an endpoint, update the spec in the same commit
Do NOT generate separate spec files per endpoint. Use a single openapi.yaml at the project root.
A practical approach is to also include a rule about code-first versus spec-first:
## Spec workflow
We use a code-first approach:
1. Write the route handler with Zod schemas for validation
2. Generate OpenAPI spec from the Zod schemas using @asteasolutions/zod-to-openapi
3. Never hand-edit the openapi.yaml directly
This ensures the spec always matches the implementation.
This prevents the common problem where the OpenAPI spec and the actual API diverge. AI tools that know about your spec workflow will generate code that fits into it rather than creating standalone spec files that nobody maintains.
A complete API rules file
Here's a starter rules file that combines the patterns above into a single document you can adapt:
# API Design Rules
## Stack
- Next.js 15 App Router (route handlers in src/app/api/)
- Drizzle ORM + PostgreSQL
- Zod for request validation
- Bearer token auth (JWT)
## REST conventions
- Plural nouns: /users, /orders, /products
- Kebab-case paths: /order-items, /user-profiles
- One level of nesting max: /users/:id/orders
- No verbs in URLs
## HTTP methods
- GET: read (never mutate), POST: create (201), PUT: replace, PATCH: update, DELETE: remove (204)
## Error format
- Shape: { error: { code: "SCREAMING_SNAKE", message: "Human readable", details?: [] } }
- Codes: VALIDATION_ERROR, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, RATE_LIMITED, INTERNAL_ERROR
## Pagination
- Cursor-based: ?limit=20&cursor=<opaque>
- Response: { data: [...], pagination: { next_cursor, has_more } }
## Auth
- Bearer token required on all non-public endpoints
- 401 for missing/invalid token, 403 for insufficient permissions
- Check auth at the handler level with requireAuth() and requireRole()
## Versioning
- URL path: /v1/users
- Adding optional fields is non-breaking, removing or renaming fields is breaking
## OpenAPI
- Code-first: Zod schemas generate the spec
- Every endpoint documented with request/response schemas and examples
Why API rules drift without shared tooling
API conventions are especially prone to drift because they span the full stack. Frontend developers consume the API, backend developers build it, and API reviewers enforce it. If any of those three groups has stale context, inconsistencies creep in.
This is the same problem that affects all AI coding rules at scale, and the solution is the same: treat your rules as shared dependencies that everyone installs from one source. See how to enforce coding standards with AI assistants for the broader pattern, and real-world cursorrules examples for more rules you can adapt.
With localskills.sh, you publish your API design rules once and install them across every repository and every AI tool your team uses:
localskills install your-team/api-design-rules --target cursor claude windsurf
When your API conventions change (say, you switch from offset to cursor pagination), you update the skill and bump the version. Everyone pulls the update with one command. No stale wiki pages, no Slack messages that get buried.
For teams that use multiple AI tools, this is especially valuable. The format differences between Cursor's .mdc files, Claude Code's CLAUDE.md, and Windsurf's .windsurfrules are handled automatically. One skill, every tool. See AI coding rules for React and Next.js for how this works with framework-specific rules alongside API rules.
Ready to lock in your API conventions across your whole team? Create your free account and publish your API design rules as a shared skill today.
npm install -g @localskills/cli
localskills login
localskills publish