API Contract Design & Versioning

📖 Concept

API contract design defines how your Android app communicates with backend services. At the senior level, you're expected to design and review API contracts, not just consume them.

RESTful API best practices for mobile:

  • Use proper HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • Consistent naming: /users/{id}/posts not /getUserPosts?userId=123
  • Return appropriate status codes (200, 201, 400, 401, 404, 500)
  • Include pagination metadata in list responses
  • Use ISO 8601 for dates, UTC timezone

API versioning strategies:

  1. URL versioning: /v1/users, /v2/users — Clearest, most common
  2. Header versioning: Accept: application/vnd.api.v2+json — Cleaner URLs
  3. Query parameter: /users?version=2 — Easy but pollutes params

Request/Response best practices:

  • Wrap responses in an envelope: { "data": {...}, "meta": {...}, "errors": [...] }
  • Use consistent error format: { "code": "USER_NOT_FOUND", "message": "...", "details": {...} }
  • Keep responses flat — avoid deeply nested objects for mobile bandwidth efficiency
  • Include only needed fields — consider GraphQL or field selection (?fields=id,name,email)

💻 Code Example

codeTap to expand ⛶
1// Retrofit API definition — clean contract
2interface UserApi {
3 @GET("v1/users/{id}")
4 suspend fun getUser(@Path("id") id: String): ApiResponse<UserDto>
5
6 @GET("v1/users/{id}/posts")
7 suspend fun getUserPosts(
8 @Path("id") userId: String,
9 @Query("page") page: Int = 1,
10 @Query("limit") limit: Int = 20,
11 @Query("sort") sort: String = "created_at:desc"
12 ): PaginatedResponse<PostDto>
13
14 @POST("v1/users")
15 suspend fun createUser(@Body request: CreateUserRequest): ApiResponse<UserDto>
16
17 @PATCH("v1/users/{id}")
18 suspend fun updateUser(
19 @Path("id") id: String,
20 @Body request: UpdateUserRequest
21 ): ApiResponse<UserDto>
22}
23
24// Standardized API response wrapper
25data class ApiResponse<T>(
26 val data: T,
27 val meta: Meta? = null
28)
29
30data class PaginatedResponse<T>(
31 val data: List<T>,
32 val pagination: Pagination
33)
34
35data class Pagination(
36 val page: Int,
37 val limit: Int,
38 val totalPages: Int,
39 val totalItems: Int,
40 val hasNext: Boolean
41)
42
43// Error response handling
44data class ApiError(
45 val code: String,
46 val message: String,
47 val details: Map<String, Any>? = null
48)
49
50// Centralized error handling interceptor
51class ErrorInterceptor : Interceptor {
52 override fun intercept(chain: Interceptor.Chain): Response {
53 val response = chain.proceed(chain.request())
54 if (!response.isSuccessful) {
55 val errorBody = response.body?.string()
56 val apiError = try {
57 Json.decodeFromString<ApiError>(errorBody ?: "")
58 } catch (e: Exception) {
59 ApiError("UNKNOWN", "HTTP ${response.code}")
60 }
61 throw ApiException(response.code, apiError)
62 }
63 return response
64 }
65}
66
67class ApiException(val httpCode: Int, val error: ApiError) : Exception(error.message)

🏋️ Practice Exercise

Practice:

  1. Design a RESTful API contract for a note-taking app with CRUD operations
  2. Implement a Retrofit interceptor that adds authorization headers and handles 401 errors
  3. Create a standardized error handling pipeline from API error to UI message
  4. Design an API versioning strategy for a 3-year-old app with 5M users
  5. Compare REST vs GraphQL for mobile — when would you choose each?

⚠️ Common Mistakes

  • Not versioning APIs from the start — forced breaking changes affect all users simultaneously

  • Returning different error formats for different endpoints — inconsistency makes error handling complex

  • Over-fetching data — returning 50 fields when the mobile app needs 5 wastes bandwidth

  • Not including pagination for list endpoints — mobile apps must handle large datasets efficiently

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for API Contract Design & Versioning. Login to unlock this feature.