Sandwich: a sealed API response library for Retrofit, Ktor and Kotlin Multiplatform
Project brief: Sandwich is an adaptable and lightweight sealed API library designed for handling API responses and exceptions in Kotlin for Retrofit, Ktor, and Kotlin Multiplatform.
At a glance
- What is it?
- Sandwich replaces hand-written Resource or Result wrappers with a sealed ApiResponse type covering success, error payloads and client-side exceptions. It is aimed at Kotlin teams who want one error-handling shape across Android and KMP networking stacks.
- Who is it for?
- Adopt Sandwich if you already use Retrofit or Ktor in Kotlin and want one sealed response type instead of a per-project Resource wrapper, and if you can live with the library's own error model rather than your existing one. Do not adopt it if your error handling is already centralised in an interceptor or a Ktor plugin, since adding ApiResponse on top duplicates that layer.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 18, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The wrapper class problem Sandwich removes
Most Kotlin networking code grows the same artefact. Someone writes a sealed class called Resource, Result or Outcome with three branches: loading or success, a server error carrying a parsed body, and a client-side throwable. The class is copied between projects, drifts, and ends up with a different shape in every repository. Sandwich's stated goal is to delete that work. The README says the library was conceived to standardise interfaces for modelling responses from Retrofit, Ktor, and whatever, and that it removes the need to create wrapper classes like Resource or Result.
The audience is narrow and specific: Kotlin developers calling HTTP APIs, mostly on Android, increasingly in Kotlin Multiplatform projects where the same response type has to compile for both an Android target and a non-Android one. If you are writing a one-off script with a single endpoint, the abstraction costs more than it returns. If you maintain several modules or several apps that talk to the same backend, a single response contract is the point.
How ApiResponse models success, error and exception
The core type is ApiResponse, an interface with three implementations. ApiResponse.Success holds data and an optional tag, described in the README as an additional value kept to distinguish the origin of the data or to help with post-processing. ApiResponse.Failure.Error represents a failed request, typically a bad request or an internal server error, and can carry an error payload. ApiResponse.Failure.Exception covers failures captured by unexpected exceptions during request creation or response processing on the client side, such as a network connection failure, and exposes the exception and a message.
The split between Error and Exception is the design decision worth noticing. A 500 with a JSON error body and a dropped connection are different problems, and collapsing them into one failure branch is a common source of bugs where retry logic fires on a validation error. Sandwich keeps them apart at the type level.
Both failure branches are open to extension. The README shows custom responses declared as data objects extending ApiResponse.Failure.Error with a payload string, and the same pattern for ApiResponse.Failure.Exception with a throwable. That means domain-specific failures such as a rate-limit response or a wrong-argument response can be first-class values rather than strings compared at the call site. The repository is split into modules matching each integration: sandwich for the core type, sandwich-retrofit, sandwich-ktor, sandwich-ktor-serialization, sandwich-ktorfit, sandwich-retrofit-serialization, sandwich-retrofit-datasource, sandwich-test and sandwich-bom.
Installing Sandwich and making a first call
Sandwich publishes to Maven Central under the group com.github.skydoves. The README's Gradle example adds the BOM first so the individual artifacts share a version, then pulls the core library and the integration you need. The 2.4.0 release is the version used in the README sample.
dependencies {
implementation(platform("com.github.skydoves:sandwich-bom:2.4.0"))
implementation("com.github.skydoves:sandwich")
implementation("com.github.skydoves:sandwich-retrofit") // For Retrofit (Android)
testImplementation("com.github.skydoves:sandwich-test") // For Testing
}For Kotlin Multiplatform the README places the dependencies in the commonMain source set and swaps the Retrofit artifact for the Ktor ones, adding sandwich-ktor, sandwich-ktor-serialization and sandwich-ktorfit, with sandwich-test in commonTest. Note that this snippet uses a $version placeholder rather than a pinned number, so you supply the BOM version yourself.
sourceSets {
val commonMain by getting {
dependencies {
implementation(project.dependencies.platform("com.github.skydoves:sandwich-bom:$version"))
implementation("com.github.skydoves:sandwich")
implementation("com.github.skydoves:sandwich-ktor")
implementation("com.github.skydoves:sandwich-ktor-serialization")
implementation("com.github.skydoves:sandwich-ktorfit")
}
}
val commonTest by getting {
dependencies {
implementation("com.github.skydoves:sandwich-test")
}
}
}The first real use is constructing the type by hand to see the shape. The README's own examples build ApiResponse.Success with a data argument, read .data back, and optionally attach a tag; ApiResponse.Failure.Exception takes an exception such as HttpTimeoutException and exposes .exception and .message; ApiResponse.Failure.Error takes a payload and exposes .payload. Wiring these into actual Retrofit or Ktor call sites is documented separately, in the Retrofit, Ktor and Ktorfit integration pages linked from the README, not in the README itself.
val apiResponse = ApiResponse.Success(data = myData)
val data = apiResponse.data
val tagged = ApiResponse.Success(data = myData, tag = myTag)
val tag = tagged.tagOne build detail matters for release builds: the README states that R8 and ProGuard rules are already bundled into the JAR via the consumer-rules.pro file, so R8 picks them up without you editing your own proguard-rules file.
Where the abstraction gets in the way
ApiResponse is a return type, not a transport mechanism. It does not intercept HTTP calls for you. Every Retrofit method or Ktor client call that should produce one has to be adapted, either through the integration modules or by hand. If your project already funnels errors through an OkHttp interceptor or a Ktor plugin, adopting Sandwich means two error paths that must agree on what counts as an error payload.
The library also imposes its own vocabulary. A team that has spent a year standardising on a Result type with a different failure taxonomy will be translating between two models, and the translation is exactly the boilerplate Sandwich exists to remove. The README does not document a migration path from an existing wrapper class, so that translation is on you.
Finally, the documentation is split across the README and a separate site. The README repeatedly defers to skydoves.github.io/sandwich for comprehensive details and lists integration pages for Retrofit, Ktor, Ktorfit and testing rather than inlining them. That is fine for a library of this size, but it means the README alone will not get you from dependency to a working call site. The repository's last push was on 2026-07-27, which is recent enough that the published documentation should track the 2.4.0 artifacts.
Sandwich against a hand-rolled Result type
The obvious alternative is the sealed class you already have. The difference is not capability, since a three-branch sealed class can express the same states, but ownership and maintenance. A hand-rolled Result lives in your repository, so you can add a branch for a domain-specific failure in the same commit that needs it, with no dependency to upgrade. Sandwich's custom failure responses are declared by extending ApiResponse.Failure.Error or ApiResponse.Failure.Exception, which the README demonstrates with data objects, so the extension point exists, but the base type is not yours to change.
The other realistic alternative is Kotlin's own Result or a library-specific error channel, which keeps you inside the standard library but has no notion of an HTTP error payload or a tag. Sandwich's tag property, described as a way to distinguish the origin of data, has no equivalent in Result. The trade is a dependency and a fixed vocabulary in exchange for one shape shared across Retrofit, Ktor and Ktorfit, which is the reason the library exists.
Licence, releases and the cost of upgrading
Sandwich is licensed under Apache-2.0, which permits commercial and closed-source use and requires that the licence and notices be preserved. That is the whole of the licence implication here; the terms are standard and the repository ships a LICENSE file at the root. This is not legal advice, and Apache-2.0 has patent and notice clauses worth reading in full if your organisation has a policy on them.
Upgrade cost is bounded by the BOM. Because the README's Gradle sample pins a single sandwich-bom version and lets the individual artifacts inherit it, moving from 2.3.0 to 2.4.0 is one line in most projects. The three recent releases, 2.2.2 on 2026-04-16, 2.3.0 on 2026-06-29 and 2.4.0 on 2026-07-27, arrived within roughly three months of each other. The README does not document a deprecation policy or a changelog location, so the release notes on GitHub are the place to check before bumping.
Editorial conclusion
Adopt Sandwich if you already use Retrofit or Ktor in Kotlin and want one sealed response type instead of a per-project Resource wrapper, and if you can live with the library's own error model rather than your existing one. Do not adopt it if your error handling is already centralised in an interceptor or a Ktor plugin, since adding ApiResponse on top duplicates that layer. Before committing, verify the BOM version you pin, check that the sandwich-retrofit or sandwich-ktor artifact matches your stack, and confirm the R8 rules bundled in the JAR are enough for your build.
Frequently asked questions
How do I install Sandwich in an Android project?
Add the sandwich-bom platform dependency to your module's build.gradle, then add com.github.skydoves:sandwich and the sandwich-retrofit artifact. The README's example pins the BOM at 2.4.0 and also lists sandwich-test as a testImplementation dependency.
Does Sandwich work with Kotlin Multiplatform?
Yes. The README gives a Kotlin Multiplatform example that places sandwich, sandwich-ktor, sandwich-ktor-serialization and sandwich-ktorfit in the commonMain source set, with sandwich-test in commonTest.
Do I need to add R8 or ProGuard rules for Sandwich?
No. The README states that the specific rules are already bundled into the JAR through consumer-rules.pro and are interpreted by R8 automatically.
What is the difference between ApiResponse.Failure.Error and ApiResponse.Failure.Exception?
Failure.Error represents a failed API or I/O request such as a bad request or internal server error and can carry an error payload. Failure.Exception covers failures captured by unexpected exceptions during request creation or response processing on the client side, such as a network connection failure, and exposes the exception.
Community notes