What Is an API Model? And What Is a Domain Model?
If you've read much about app architecture, you've hit sentences like "map your API models into your domain layer" and wondered whether that's a real distinction or just vocabulary people use to sound serious.
It's a real distinction, and it's simpler than the language around it suggests. This post defines the terms plainly. If you already know what they mean and want the full implementation with mapping layers and protocols, I wrote that up separately in Domain Models vs API Models in Swift.
The Short Definitions
An API model is a Swift type whose shape is dictated by your server. Its job is to decode a JSON response correctly. It exists because the network gave you a specific structure and you need something to parse it into.
A domain model is a Swift type whose shape is dictated by your app. Its job is to represent a concept in your product in the way your app wants to think about it. It exists because your features need something convenient to work with.
The domain layer is the part of your codebase holding domain models and the rules that operate on them. It's the part that would survive unchanged if you switched backends tomorrow.
That's the distinction. The rest of this post is why it earns its keep.
Why Would You Want Two Types for One Thing?
Here's a landmark as your server sends it:
{
"landmark_id": 4021,
"landmark_name": "Half Dome",
"loc_state": "California",
"is_fav": 0,
"created_ts": 1735689600,
"img_url": null
}
The API model matches that exactly, because its only job is decoding without throwing:
struct LandmarkAPIModel: Decodable {
let landmarkID: Int
let landmarkName: String
let locState: String
let isFav: Int // 0 or 1, not a Bool
let createdTS: Int // Unix timestamp, not a Date
let imgURL: String? // Sometimes null
enum CodingKeys: String, CodingKey {
case landmarkID = "landmark_id"
case landmarkName = "landmark_name"
case locState = "loc_state"
case isFav = "is_fav"
case createdTS = "created_ts"
case imgURL = "img_url"
}
}
Now here's what your app actually wants:
struct Landmark: Identifiable, Hashable {
let id: Int
let name: String
let state: String
let isFavorite: Bool
let createdAt: Date
let imageURL: URL?
}
Look at the differences. isFav: Int became isFavorite: Bool. createdTS: Int became createdAt: Date. imgURL: String? became imageURL: URL?. Abbreviations became words.
Every one of those conversions has to happen somewhere. The question isn't whether you do this work - it's whether you do it once, at the boundary, or scattered across forty view files that each write landmark.isFav == 1.
What Happens Without the Split
Use the API model directly throughout your app and the server's shape leaks into everything:
// In a view
if landmark.isFav == 1 {
Image(systemName: "star.fill")
}
// In a filter
landmarks.filter { $0.isFav == 1 }
// In a sort
landmarks.sorted { $0.createdTS > $1.createdTS }
Then the backend team ships v2. is_fav becomes a proper boolean favorited. Now you have a compiler error in every one of those places, and the fix in each is mechanical, tedious, and easy to get subtly wrong.
With the split, the change is confined to one file:
extension LandmarkAPIModel {
func toDomain() -> Landmark {
Landmark(
id: landmarkID,
name: landmarkName,
state: locState,
isFavorite: favorited, // <- the only line that changes
createdAt: Date(timeIntervalSince1970: TimeInterval(createdTS)),
imageURL: imgURL.flatMap(URL.init(string:))
)
}
}
Your views never knew the server existed, and that single confined edit is the argument for the whole approach.
So What Is a "Domain API"?
This phrase confuses people because it gets used two different ways.
Sometimes it means the interface your domain layer exposes - the set of functions your features call to get work done, stated in domain terms:
protocol LandmarkRepository {
func allLandmarks() async throws -> [Landmark]
func toggleFavorite(_ landmark: Landmark) async throws
}
Notice there's no URL, no JSON, no status code in that signature. It's phrased entirely in your app's vocabulary. That's a domain API: your features depend on it, and it depends on the network.
Sometimes it means an API organized around business concepts rather than database tables - a backend design idea (/landmarks/4021/favorite rather than /rows/update). That's a server-side concern and mostly not your problem as a client developer.
When an iOS post says "domain API," it nearly always means the first one.
Related Terms You'll See
DTO (Data Transfer Object) is another name for an API model. Same concept, older vocabulary, common in Java and .NET codebases. If someone says DTO, read "API model."
Entity usually means a persistence model - the type your database layer stores. In SwiftData or Core Data, this is a third shape, distinct from both your API model and your domain model. Yes, that can mean three representations of a landmark. That's normal and it's still less work than untangling them later.
Mapper or mapping layer is the code that converts between these. In Swift it's usually just a toDomain() method, not a whole framework.
Repository is a type that hands your app domain models and hides where they came from - network, cache, disk, or all three. The protocol above is one.
When You Don't Need This
I'd rather you skip the split deliberately than adopt it as ritual.
Skip it when your API model and domain model would be identical. If your server returns clean, well-named JSON that already matches how your app thinks, writing two identical structs and a mapper between them is pure ceremony. Use one type. Add the split the day they diverge - and you'll know the day, because you'll find yourself writing a conversion at a call site.
Skip it for a prototype. If the app might not exist in a month, the flexibility isn't worth the file count.
Keep it when any of these are true: the server's shape is awkward or inconsistent; multiple endpoints return the same concept differently; you're consuming a third-party API you don't control; the backend is under active development; or you have more than a handful of screens.
That last one is the practical trigger. The cost of the split is roughly fixed. The cost of not having it scales with how many places touch the data.
The One-Paragraph Version
An API model is shaped by the server and exists to decode JSON. A domain model is shaped by your app and exists to be convenient for your features. You convert between them at one boundary so that server changes stop at that boundary instead of rippling through your views. The domain layer is where domain models and your business rules live, and it's the part of your app that doesn't care what backend you use.
If that lands and you want to see it built out properly - protocols, mapping, write requests, and why it removes the need for a view model layer - that's the full post.