Skip to content
HomeWorkProcessAboutBlogContact[email protected]
← Back to blog

API Design Best Practices for Business Applications

API Design Best Practices for Business Applications. API design Insights from The Proper Motion Company.

APIs are the connective tissue of modern business software. Every integration, mobile app, partner connection, and internal service depends on APIs that are well-designed, predictable, and maintainable. Yet API design is treated as an afterthought in many projects – the team builds the UI first, then exposes whatever endpoints the frontend happened to need. The result is an API that’s tightly coupled to one client, inconsistent across endpoints, and painful to extend. Here’s how to design APIs that serve your business for years, not just your current sprint.

Resource Modeling: Think in Nouns, Not Verbs

The most common API design mistake is modeling endpoints around actions rather than resources. You end up with /createUser, /updateUserEmail, /getUserOrders, /cancelOrder – a collection of RPC-style endpoints that each do one thing and don’t compose into a coherent data model.

REST’s core insight is that your API should expose resources (nouns) and let HTTP methods (verbs) express the operations. A user is a resource at /users/{id}. Creating one is POST /users. Reading one is GET /users/{id}. Updating one is PATCH /users/{id}. This isn’t pedantic – it produces APIs that are predictable. A developer who has worked with GET /users/{id} can correctly guess how GET /invoices/{id} works without reading any documentation.

For business applications, resource modeling requires careful thought about your domain entities. Start with the core entities: users, organizations, invoices, products, orders. Each gets a top-level resource path. Related entities nest naturally: /organizations/{id}/members, /orders/{id}/line-items, /invoices/{id}/payments.

Avoid deep nesting beyond two levels. /organizations/{orgId}/teams/{teamId}/members/{memberId}/permissions is too deep – it creates long, fragile URLs and implies a rigid hierarchy that may not match all access patterns. Instead, expose members as a top-level resource (/members/{id}) with query parameters for filtering by organization or team.

When you genuinely need an action that doesn’t map cleanly to CRUD, use a sub-resource that represents the action’s result. Instead of POST /orders/{id}/cancel, consider POST /orders/{id}/cancellation – the cancellation is a resource that was created. This maintains the resource-oriented model while supporting non-CRUD operations.

Related: Building a Developer Documentation Portal

Pagination, Filtering, and the Performance Cliff

Business applications deal with large datasets. An API that returns all 50,000 invoices in a single response will work in development and fail catastrophically in production. Pagination, filtering, and sorting must be designed into every list endpoint from the beginning.

Cursor-based pagination outperforms offset-based pagination for almost every business use case. Offset pagination (?page=3&per_page=25) has a fundamental problem: if records are inserted or deleted between page requests, items shift between pages. A user paging through invoices might see the same invoice twice or miss one entirely. Cursor pagination (?after=inv_abc123&limit=25) anchors to a specific record and returns the next N records after it, which is stable regardless of concurrent modifications.

The response format should include pagination metadata:

{
  "data": [...],
  "pagination": {
    "has_more": true,
    "next_cursor": "inv_xyz789",
    "total_count": 48230
  }
}

Include total_count only when it’s cheap to compute. For tables with millions of rows, COUNT(*) can be expensive. Consider providing an approximate count or omitting it for very large collections.

Filtering should follow a consistent pattern across all list endpoints. A simple approach: allow filtering on any indexed field via query parameters. GET /invoices?status=overdue&customer_id=cust_123&created_after=2025-01-01. Document which fields are filterable for each endpoint. Don’t allow filtering on non-indexed fields – it’s a performance trap that works fine in testing and causes database timeouts in production.

Sorting follows a similar pattern: ?sort=created_at&order=desc. Support sorting on indexed fields only. For multi-field sorting, use comma-separated values: ?sort=status,created_at&order=asc,desc.

Error Responses That Help Developers Debug

API error responses are documentation. A good error response tells the developer exactly what went wrong and how to fix it. A bad error response sends them on a debugging expedition.

Adopt a structured error format and use it consistently across every endpoint:

{
  "error": {
    "code": "validation_error",
    "message": "The request body contains invalid fields.",
    "details": [
      {
        "field": "email",
        "issue": "must be a valid email address",
        "value": "not-an-email"
      },
      {
        "field": "plan",
        "issue": "must be one of: starter, professional, enterprise",
        "value": "premium"
      }
    ],
    "request_id": "req_7f3a9b2c"
  }
}

Key principles: use machine-readable error codes (validation_error, not_found, rate_limited, insufficient_permissions) alongside human-readable messages. Include a request ID that correlates to your server logs so support teams can trace a specific failure. For validation errors, enumerate every field that failed, not just the first one – developers hate fixing one error, resubmitting, and discovering the next error.

HTTP status codes should be meaningful and consistent. 400 for client errors (bad input), 401 for unauthenticated requests, 403 for unauthorized requests (authenticated but lacking permission), 404 for resources that don’t exist, 409 for conflict (trying to create a resource that already exists), 422 for semantically invalid input (syntactically valid JSON but business logic violations), and 429 for rate limiting. Reserve 500 for genuine server errors that the client can’t fix.

See also: The AI Technology Stack: Models, Frameworks, and Infrastructure Guide

Versioning Strategy for Long-Lived APIs

Business APIs live for years. Partners build integrations against them. Internal tools depend on them. You will need to make breaking changes eventually, and your versioning strategy determines whether that’s a smooth transition or a crisis.

URL-based versioning (/v1/users, /v2/users) is the most explicit and easiest to understand. Each major version is a separate set of endpoints. Clients opt into a new version when they’re ready. The downside: you’re maintaining multiple versions of your API simultaneously, and the deployment infrastructure needs to route requests to the correct version’s handlers.

Header-based versioning (Accept: application/vnd.myapp.v2+json) keeps URLs clean but is less visible in logs and documentation. It’s harder for developers to test with a browser or curl since they need to set headers explicitly.

Our recommendation for business applications: use URL-based versioning with a strict deprecation policy. When you release v2, announce a deprecation timeline for v1 (typically 12-18 months for B2B APIs). During the deprecation period, v1 continues to work but returns a Deprecation header with the sunset date. After the sunset date, v1 returns 410 Gone.

Minimize the need for version bumps by designing for extensibility. Adding new fields to a response is not a breaking change. Adding new optional parameters to a request is not a breaking change. Adding new endpoints is not a breaking change. The changes that force a version bump are: removing or renaming fields, changing field types, changing the meaning of a field, removing endpoints, and changing authentication mechanisms. If you design your initial API with optional fields and extensible schemas, you can evolve it significantly within a single version.

Authentication and Rate Limiting for Production APIs

API authentication for business applications needs to support multiple use cases: user-initiated requests (from the frontend), server-to-server integration (from partner systems), and automated processes (from cron jobs or background workers).

API keys are appropriate for server-to-server integration. They’re simple, stateless, and easy for partners to manage. Issue keys per integration (not per organization), so you can revoke a compromised integration without affecting others. Prefix keys with an environment indicator: sk_live_... for production, sk_test_... for sandbox. This prevents accidental production calls from test environments.

JWT tokens suit user-initiated requests. The frontend authenticates the user, receives a short-lived JWT (15-60 minutes), and includes it in API requests. The API validates the JWT signature and extracts user context without a database lookup. Pair JWTs with refresh tokens for session continuity.

Rate limiting protects your infrastructure and ensures fair usage. Implement rate limits per API key or user, not per IP address (IPs are shared, spoofable, and unstable). Use a sliding window algorithm: “100 requests per minute per API key.” Return rate limit headers on every response:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1640995200

When the limit is exceeded, return 429 with a Retry-After header. For business APIs, consider tiered rate limits based on pricing plan – your enterprise customers generating revenue need higher limits than free-tier integrations.

Documentation as a Product, Not an Afterthought

API documentation is the developer experience. Poor documentation drives developers to competitors or to building workarounds that increase your support burden. Good documentation is a product asset that reduces support costs and accelerates partner integrations.

Generate documentation from your API specification (OpenAPI/Swagger), but supplement it with hand-written guides. The specification documents what the API does; the guides explain how to use it. Every API should have: a getting started guide (authentication, first request, common patterns), endpoint reference (auto-generated from the spec), code examples in at least three languages (Python, JavaScript, and your customers’ most common stack), a changelog (what changed in each release, with migration notes for breaking changes), and error handling guide (common errors and their solutions).

Interactive documentation (try-it-in-the-browser tools like Swagger UI or Redocly) lets developers experiment without writing code. Provide a sandbox environment with test data so developers can explore the API safely. The sandbox should behave identically to production except with fake data and no real-world side effects.

Invest in your documentation as continuously as you invest in your code. Every API change should include a documentation update in the same pull request. Stale documentation is worse than no documentation – it builds false confidence that leads to harder debugging.


If you’re designing an API for your business application and want it built for the long term, we’d like to help. We design APIs that partners love integrating with and that scale gracefully as your business grows.

// Related posts