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}/postsnot/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:
- URL versioning:
/v1/users,/v2/users— Clearest, most common - Header versioning:
Accept: application/vnd.api.v2+json— Cleaner URLs - 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
1// Retrofit API definition — clean contract2interface UserApi {3 @GET("v1/users/{id}")4 suspend fun getUser(@Path("id") id: String): ApiResponse<UserDto>56 @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>1314 @POST("v1/users")15 suspend fun createUser(@Body request: CreateUserRequest): ApiResponse<UserDto>1617 @PATCH("v1/users/{id}")18 suspend fun updateUser(19 @Path("id") id: String,20 @Body request: UpdateUserRequest21 ): ApiResponse<UserDto>22}2324// Standardized API response wrapper25data class ApiResponse<T>(26 val data: T,27 val meta: Meta? = null28)2930data class PaginatedResponse<T>(31 val data: List<T>,32 val pagination: Pagination33)3435data class Pagination(36 val page: Int,37 val limit: Int,38 val totalPages: Int,39 val totalItems: Int,40 val hasNext: Boolean41)4243// Error response handling44data class ApiError(45 val code: String,46 val message: String,47 val details: Map<String, Any>? = null48)4950// Centralized error handling interceptor51class 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 response64 }65}6667class ApiException(val httpCode: Int, val error: ApiError) : Exception(error.message)
🏋️ Practice Exercise
Practice:
- Design a RESTful API contract for a note-taking app with CRUD operations
- Implement a Retrofit interceptor that adds authorization headers and handles 401 errors
- Create a standardized error handling pipeline from API error to UI message
- Design an API versioning strategy for a 3-year-old app with 5M users
- 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.