Table of Contents
Introduction
Retrofit becomes powerful when you describe your web API as Kotlin interfaces. In this chapter you focus on how to define these API interfaces, how Retrofit turns them into network calls, and how to correctly map HTTP operations, URLs, parameters, headers, and request or response bodies.
What an API Interface Is in Retrofit
An API interface in Retrofit is a Kotlin interface where each function represents a single HTTP request. You do not write the networking code manually. Instead, you declare what the request looks like, and Retrofit generates the implementation at runtime when you call retrofit.create(YourApi::class.java).
Each function in the interface is usually a suspend function or returns a Call<T>, and is annotated to describe the HTTP method, endpoint path, and any parameters.
Important:
An API interface only declares the shape of requests and responses. Retrofit creates the real implementation. You never instantiate the interface with YourApi() but always through retrofit.create(...).
HTTP Method Annotations
Retrofit provides annotations that map to HTTP methods. The most common are @GET, @POST, @PUT, @DELETE, and @PATCH. You place these above each function to indicate which HTTP method should be used.
A simple example of a GET request is:
interface UserApi {
@GET("users")
suspend fun getUsers(): List<User>
}
Here, Retrofit will call GET https://your-base-url/users if the base URL is https://your-base-url/.
For a POST request that sends data in the request body you might have:
interface UserApi {
@POST("users")
suspend fun createUser(
@Body user: User
): User
}Each method annotation can include a relative path, which is appended to the base URL defined when you created the Retrofit instance.
Relative Paths and Path Parameters
Sometimes you need to include dynamic values directly in the URL path, such as an ID. Retrofit uses @Path for this purpose.
You declare a placeholder in the path string using {name} and match it with a function parameter annotated with @Path("name").
interface UserApi {
@GET("users/{id}")
suspend fun getUserById(
@Path("id") userId: Int
): User
}
Retrofit replaces {id} with the value of userId and automatically encodes it. The placeholder name inside {} must match the string inside @Path("...").
You can use multiple @Path parameters when the URL has more than one segment that changes.
Query Parameters
Query parameters appear at the end of a URL after ?, for example ?page=2&size=20. Retrofit uses @Query and @QueryMap for these.
A single query parameter is straightforward:
interface UserApi {
@GET("users")
suspend fun getUsersByPage(
@Query("page") page: Int,
@Query("size") size: Int
): List<User>
}
Retrofit will produce URLs like users?page=1&size=20.
If you have a dynamic set of query parameters, use @QueryMap with a Map<String, String>:
interface UserApi {
@GET("users")
suspend fun searchUsers(
@QueryMap filters: Map<String, String>
): List<User>
}Retrofit converts each map entry into a query parameter.
Request Body with `@Body`
For requests that send JSON content, such as POST or PUT, you usually pass a Kotlin data class to @Body. Retrofit uses the configured converter (for example Moshi or Gson) to turn this object into JSON.
interface AuthApi {
@POST("login")
suspend fun login(
@Body credentials: LoginRequest
): LoginResponse
}
Here, LoginRequest and LoginResponse are Kotlin data classes. Retrofit serializes LoginRequest into JSON and deserializes the server JSON into LoginResponse.
You normally use a single @Body per function. Retrofit does not allow multiple @Body parameters in one method.
Headers and `@Header`
HTTP headers carry metadata such as authorization tokens, content type, or custom information. Retrofit lets you define headers in several ways. The most flexible way inside the interface is with @Header and @HeaderMap.
To add a dynamic header, define a parameter with @Header("Header-Name"):
interface AuthApi {
@GET("profile")
suspend fun getProfile(
@Header("Authorization") token: String
): UserProfile
}
When you call this function, you pass something like "Bearer your_token_here" as the argument.
If you need to send multiple headers that are not known at compile time, use @HeaderMap:
interface AuthApi {
@GET("profile")
suspend fun getProfileWithHeaders(
@HeaderMap headers: Map<String, String>
): UserProfile
}
You can also define constant headers at the method level with the @Headers annotation, but that is configured on the method itself rather than as parameters.
Sending Form Data
Some APIs expect form encoded data, where the request body looks like a web form submission, not JSON. For this pattern Retrofit uses @FormUrlEncoded on the method combined with @Field or @FieldMap parameters.
interface AuthApi {
@FormUrlEncoded
@POST("login")
suspend fun loginWithForm(
@Field("username") username: String,
@Field("password") password: String
): LoginResponse
}
Retrofit creates a body like username=john&password=secret with a content type similar to application/x-www-form-urlencoded.
For a flexible form, @FieldMap allows you to supply a map of fields:
interface AuthApi {
@FormUrlEncoded
@POST("login")
suspend fun loginWithFormMap(
@FieldMap fields: Map<String, String>
): LoginResponse
}
@Body is not used together with @FormUrlEncoded in the same method.
Multipart Requests and File Upload
When you need to upload files or a mix of files and form fields, you typically use multipart requests. Retrofit supports these with @Multipart on the method, and @Part or @PartMap parameters.
A basic file upload might look like this:
interface UploadApi {
@Multipart
@POST("upload")
suspend fun uploadImage(
@Part image: MultipartBody.Part
): UploadResponse
}
In this pattern, you usually build the MultipartBody.Part outside the interface using OkHttp utilities. You can also include textual parts as separate @Part parameters, which Retrofit converts into appropriate multipart parts.
Multipart requests are especially useful for images, videos, or other binary content that need to be sent along with extra data such as descriptions or user IDs.
Return Types and `Response<T>`
In interfaces, the return type of each function defines how Retrofit delivers the result to you. With coroutines, you often use suspend fun and return a type that represents the parsed body.
For simple success cases you might write:
interface UserApi {
@GET("users/{id}")
suspend fun getUserById(
@Path("id") userId: Int
): User
}
This assumes the call is successful and the server returns a User. When the server might respond with errors and you want more information, you can return Response<T> and inspect the HTTP details yourself:
interface UserApi {
@GET("users/{id}")
suspend fun getUserByIdSafe(
@Path("id") userId: Int
): Response<User>
}
In this case the body is wrapped, and you can check things like response.isSuccessful, status code, and headers. How to handle those responses belongs to error handling, but the important part here is that your interface return type controls how much information Retrofit gives you.
Without coroutines, you can use Call<T> instead of suspend fun, but that style is separate from the coroutine approach.
Combining Path, Query, and Body in One Method
Real endpoints often combine several components. You might have a URL with a dynamic ID, some optional query parameters, and a JSON body in POST or PUT requests.
An example could look like this:
interface ArticleApi {
@PUT("articles/{id}")
suspend fun updateArticle(
@Path("id") articleId: Long,
@Query("notifySubscribers") notify: Boolean,
@Body updateRequest: ArticleUpdateRequest
): Article
}
Retrofit uses articleId to build the path, notify as a query parameter, and updateRequest as the JSON body.
You must make sure each URL placeholder has a corresponding @Path parameter and that query names and field names match what the server expects.
Using Different Base URLs Per Method
Sometimes some endpoints live under a different host or base path than the rest of your API. Retrofit allows you to override the base URL for a specific method using a full URL in the method annotation.
For example, if your Retrofit instance has a base URL for your main API, but one method needs a different host, you can write:
interface MixedApi {
@GET("users")
suspend fun getUsers(): List<User>
@GET("https://images.example.com/banners")
suspend fun getBanners(): List<Banner>
}
In this case, getBanners ignores the default base URL and uses the absolute URL you provided. This approach can be useful in special cases, but for most projects keeping one base URL per Retrofit instance is simpler.
Optional Parameters and Default Values
Kotlin allows default parameter values and nullable types, which works nicely with Retrofit for optional query parameters or headers. If a parameter is null, Retrofit skips it by default for many annotations such as @Query and @Header.
For example:
interface SearchApi {
@GET("search")
suspend fun search(
@Query("q") query: String,
@Query("page") page: Int? = null,
@Query("size") size: Int? = null
): SearchResult
}
Calling search("kotlin") creates a URL without page and size. Calling search("kotlin", page = 2) includes only page=2 in the query string.
This pattern makes your interface functions easier to call without creating many overloads.
Best Practices for API Interface Design
Well designed API interfaces make your networking code easier to understand and maintain. Each interface should usually group related endpoints. For example, you might have UserApi, AuthApi, and ArticleApi instead of one huge interface with all endpoints.
Function names should describe the action clearly, for example getUserById, createUser, or searchArticles, instead of generic names like call1 or loadData. Keeping the arguments aligned with the API documentation, such as using the same parameter names as in the server docs, makes the code self documenting.
Rule:
Design API interfaces so that each method represents one clear HTTP operation for a specific endpoint. Group related endpoints in separate interfaces and use descriptive method names. This keeps your networking layer readable and easier to maintain.
Finally, keep model classes and serialization details consistent. If you change the data class that represents a response, update the corresponding interface methods. The interface is the contract between your app and the server, so accuracy and clarity here prevent many runtime issues.