Free ToolBy GitIntel

REST API Design Best Practices: Decisions You'll Live With for Years

The naming, versioning, error, and pagination patterns that reduce client friction and survive as your API grows.

GitIntel tracks AI-generated code across your entire git history — giving every tool on this page the attribution layer that standard dev tooling misses.

Try GitIntel free

A REST API is a public contract. Every design decision you make is a commitment to everyone who builds on it. The patterns that cause the most regret in production are the same ones every team learns the hard way: inconsistent naming, undocumented error codes, breaking versioning strategies, and pagination designs that fail at scale.

Naming and resource modeling: use plural nouns for resource collections (`/users`, not `/user`). Represent relationships with nested paths up to one level deep (`/users/{id}/posts`), then switch to query parameters for deeper nesting. Use kebab-case for URL paths (`/api/rate-limits`), camelCase for JSON property names (`userId`, `createdAt`). Return resource identifiers as strings, not integers — integer IDs leak database implementation details and cause precision loss in JavaScript's number type above 2^53.

Error format: use RFC 7807 (Problem Details for HTTP APIs) as your error schema. Every error response includes `type` (a URI identifying the error class), `title` (human-readable summary), `status` (HTTP status code), `detail` (instance-specific description), and optionally `instance` (URI for the specific error occurrence). Clients can pattern-match on `type` for error handling logic. This is the format Stripe, GitHub, and most modern APIs use in some form.

Versioning: URI versioning (`/v1/users`) is the most explicit and caching-friendly. Header versioning (`Accept: application/vnd.api+json; version=2`) is cleaner but harder to test. Pick URI versioning and commit to it. Never break a versioned endpoint — add a new version rather than changing existing behavior. Keep older versions alive for at least 12-18 months with documented sunset dates.

Pagination: cursor-based pagination is strictly better than offset pagination for any collection that can grow or change during traversal. Offset pagination returns wrong results if items are inserted or deleted mid-traversal. Use opaque cursor tokens (`?after=dXNlcjox`) that clients don't interpret. Return `nextCursor` in the response; absence of the field means no more results. For small, stable collections (under 1K items), offset pagination is acceptable.

Authentication: Bearer tokens in the Authorization header for all API authentication. Use JWTs for stateless authorization (no database lookup per request) or opaque tokens with a lookup store. API keys for machine-to-machine: prefix them with a human-readable identifier (`sk_live_`, `ghp_`) so security scanners can detect leaks. Never put credentials in query parameters — they appear in server logs.

Rate limiting response headers: return `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` on 429 responses. These are not standardized by RFC but are widely adopted. Without them, clients have no choice but to implement exponential backoff blindly.

Frequently Asked Questions

Should I use UUID or integer IDs for API resources?

UUIDs (v4 or v7) for public-facing IDs, integers for internal database primary keys. UUIDs are non-guessable, don't leak record count, and work across distributed systems without coordination. UUID v7 is time-ordered, which makes database indexing more efficient than random UUID v4. Return UUIDs as strings in your API — JavaScript's number type cannot represent 64-bit integers precisely.

How do I handle API breaking changes without versioning the entire API?

Use additive changes where possible: adding new fields, new endpoints, or new optional parameters is non-breaking. Breaking changes (removing fields, changing types, modifying existing behavior) require a new version. For large APIs, use field-level deprecation: add a `Deprecated-At` or `X-Deprecated` header on responses that include deprecated fields, document the removal timeline, and run deprecation analytics to know when all clients have migrated.

Is OpenAPI spec worth writing for internal APIs?

Yes. OpenAPI (formerly Swagger) enables automatic client generation (openapi-generator, oapi-codegen), interactive documentation (Swagger UI, Redoc), and contract testing (Prism, Dredd). The upfront cost of writing the spec is recouped in reduced client integration time and fewer 'how does this endpoint work' questions. For TypeScript services, use code-first generation (tsoa, nestjs/swagger) to generate the spec from your types rather than maintaining it manually.

Start Using GitIntel Free

Open source. No account required. Works on any git repository.