A well-designed JSON API is predictable. Developers can guess how new endpoints behave before reading the documentation. A poorly designed one requires constant reference to docs, produces subtle bugs from inconsistent naming, and breaks clients when fields change. The cost of bad API design compounds over time —every client that integrates against it inherits the technical debt.

There are three common JSON key naming styles: snake_case (used by Python, GitHub API, Stripe), camelCase (used by JavaScript-heavy APIs, Twilio, Salesforce), and kebab-case (rare in JSON, common in URL paths). The choice matters less than consistency. Pick one for your organization and enforce it with linting. Mixing conventions within a single API —userId in one endpoint, user_id in another —is the worst outcome.

Standard Response Envelope

{



  "success": true,



  "data": {



    "id": "usr_abc123",



    "email": "[email protected]",



    "name": "Alice Chen",



    "created_at": "2026-05-01T08:00:00Z"



  },



  "meta": {



    "request_id": "req_xyz789",



    "version": "2.0",



    "timestamp": "2026-05-01T09:00:00Z"



  }



}







// Error response —same structure



{



  "success": false,



  "error": {



    "code": "VALIDATION_ERROR",



    "message": "Email address is already registered",



    "details": [



      {



        "field": "email",



        "rule": "unique",



        "message": "This email is already in use"



      }



    ]



  },



  "meta": {



    "request_id": "req_abc456",



    "version": "2.0",



    "timestamp": "2026-05-01T09:00:01Z"



  }



}

Pagination Patterns

{



  "data": [



    { "id": "usr_001", "name": "Alice" },



    { "id": "usr_002", "name": "Bob" },



    { "id": "usr_003", "name": "Carol" }



  ],



  "pagination": {



    "cursor": "eyJpZCI6InVzcl8wMDMifQ",



    "has_next": true,



    "has_prev": false,



    "limit": 20,



    "total": 1542



  }



}







// Client requests next page:



// GET /api/users?cursor=eyJpZCI6InVzcl8wMDMifQ&limit=20







// Offset-based (simpler but degrades at scale):



{



  "pagination": {



    "page": 1,



    "per_page": 20,



    "total_pages": 78,



    "total_count": 1542



  }



}

Versioning Strategies

StrategyExampleProsCons
URL path/api/v2/usersExplicit, cache-friendlyURL pollution
HeaderAPI-Version: 2.0Clean URLsHarder to test in browser
Query param/api/users?version=2Easy to testCan be ignored by proxies
Accept headerAccept: application/vnd.api+json;version=2RESTful purist approachComplex for clients
// Version in URL path —most practical choice



GET /api/v1/users         // Legacy version



GET /api/v2/users         // Current version with breaking changes



GET /api/v2/users/:id/orders  // Consistent prefix







// Rules for breaking vs non-breaking changes:



// Non-breaking (safe to add without version bump):



// - Adding new optional fields to responses



// - Adding new optional query parameters



// - Adding new endpoints







// Breaking changes (REQUIRE version bump):



// - Renaming or removing fields



// - Changing field types



// - Changing error response format



// - Making previously optional fields required







// Deprecation header for old versions:



// Deprecation: true



// Sunset: Sat, 1 Jan 2027 00:00:00 GMT

Boolean and Null Handling

Use native JSON booleans (true/false), not strings ("true"/"false") or integers (1/0). Reserve null for genuinely missing or unknown values —avoid using it to mean 'not applicable' when omitting the field entirely is cleaner. Never return undefined in JSON responses; serialize all defined fields explicitly.

Date and Time Fields

// Always use ISO 8601 with timezone for timestamps



{



  "created_at": "2026-05-01T09:00:00Z",         // UTC preferred



  "updated_at": "2026-05-01T09:15:00Z",



  "scheduled_for": "2026-05-02T14:00:00+08:00",  // Local time with offset



  "expires_at": "2026-05-08T09:00:00Z",







  // For dates without time: use date-only format



  "birth_date": "1990-03-15",



  "fiscal_year_end": "2026-12-31",







  // For durations: use ISO 8601 duration or seconds



  "session_duration": "PT30M",   // ISO 8601: 30 minutes



  "timeout_seconds": 1800         // Or just seconds as integer



}

ID Formats

ID FormatExampleBest For
UUID v4550e8400-e29b-41d4-a716-446655440000Distributed systems, privacy
Prefixed IDusr_abc123xyzMulti-entity APIs (Stripe style)
Sequential int12345Simple apps, SQL-native
ULID01ARZ3NDEKTSV4RRFFQ69G5FAVTime-sortable, UUID-compatible
NanoidV1StGXR8_Z5jdHi6B-myTURL-safe, compact

Key Takeaways

  • Consistency beats perfection —pick a naming convention (snake_case or camelCase) and enforce it across the entire API
  • Use a standard response envelope with success, data, error, and meta fields
  • Always include timestamps in ISO 8601 format with timezone information
  • Version your API in the URL path for clarity and cache-friendliness
  • Use cursor-based pagination for large datasets; offset pagination degrades past a few thousand records
  • Prefixed IDs (usr_abc123) prevent mixing ID types across different entity types