7.12. Response Models
Table of Contents
Why Response Models Matter
When you build REST APIs, you are constantly sending data back to clients: users, lists of items, error messages, metadata, and more.
A response model is a clear definition of what your API returns in each situation.
You can think of it as a contract:
- The endpoint says: "If you call me correctly, I will return data that looks exactly like this."
- The client relies on that contract to parse and display the data.
Response models are important because they:
- Make APIs predictable and easier to use.
- Help you avoid leaking sensitive fields (like passwords).
- Keep responses consistent across many endpoints.
- Act as documentation for frontend developers and other API consumers.
In code, a response model is usually a class or type that describes the shape of the data you return.
Example idea (language agnostic):
UserResponse:
id: integer
email: string
full_name: string
created_at: datetimeThis tells everyone: whenever we send a user, it will have exactly these fields and types.
Response vs Request Models
Often you will have different models for:
- Input: what the client sends in the request body.
- Output: what the API sends back in the response body.
They are usually not identical.
Typical differences:
| Concern | Request Model (Input) | Response Model (Output) |
|---|---|---|
| Sensitive data | May contain raw password, token, etc. | Must never include raw password or secrets |
| Server-generated fields | Client does not send id, timestamps | API usually returns id, created_at, etc. |
| Optional fields | Some fields optional on creation | Some fields always present after creation |
| Computed/derived fields | Usually not sent by client | May include extra computed info (e.g. age) |
Example conceptual models:
UserCreateRequest:
email: string
password: string
full_name: string (optional)
UserResponse:
id: integer
email: string
full_name: string
created_at: datetimeKey idea:
Never reuse your request models as response models when they contain sensitive data like passwords, access tokens, or internal details.
Structuring Response Data
There are many ways to structure API responses. Here are some common patterns.
Flat Object
For resources that map directly to a single record, a simple JSON object is typical:
{
"id": 42,
"title": "Learn Backend",
"completed": false
}This corresponds to a response model like:
TodoResponse:
id: integer
title: string
completed: booleanWrapped Object
Some teams always wrap the main data in a top-level field, often data:
{
"data": {
"id": 42,
"title": "Learn Backend",
"completed": false
}
}Why do this?
- Makes it easy to add
meta,errors, orlinksalongsidedatawithout changing the client’s parsing logic. - Can provide a consistent top-level structure across all endpoints.
For example:
{
"data": {
"id": 42,
"title": "Learn Backend",
"completed": false
},
"meta": {
"request_id": "abc-123",
"served_by": "backend-1"
}
}Collections (Lists)
When returning a list of items, you might:
Option 1, raw array:
[
{ "id": 1, "title": "Task 1", "completed": false },
{ "id": 2, "title": "Task 2", "completed": true }
]Option 2, wrapped with metadata:
{
"items": [
{ "id": 1, "title": "Task 1", "completed": false },
{ "id": 2, "title": "Task 2", "completed": true }
],
"total": 57,
"page": 2,
"page_size": 20
}
In this case, your response model describes the whole structure, including items and pagination fields, not just each item.
Hiding Sensitive and Internal Fields
Your database models often contain more fields than you want to expose in the API.
Example database-like record:
{
"id": 5,
"email": "user@example.com",
"password_hash": "$argon2id$v=19$m=65536,t=3,p=4$...",
"role": "admin",
"is_deleted": false,
"created_at": "2026-01-01T10:00:00Z",
"updated_at": "2026-01-01T10:05:00Z"
}If you simply return this whole record, you leak:
password_hash(dangerous).- Implementation details like
is_deleted.
A safe response model:
UserResponse:
id: integer
email: string
role: string
created_at: datetimeExample JSON response:
{
"id": 5,
"email": "user@example.com",
"role": "admin",
"created_at": "2026-01-01T10:00:00Z"
}Important rule:
Only include fields in response models that you are explicitly willing to expose to every client who can access that endpoint.
Even if data is "hashed" or "encrypted," do not expose it unless there is a clear business need.
Consistent Shapes Across Endpoints
Clients are much easier to write and maintain if you keep response shapes consistent.
Examples of consistency:
- All endpoints that return a user should use the same
UserResponsemodel, not slightly different variations. - All list endpoints can follow the same pattern, such as:
{
"items": [ /* array of resource */ ],
"total": 123,
"page": 1,
"page_size": 20
}If you keep changing the structure, the frontend must add many special cases.
Good vs Bad Example
Suppose you have three endpoints:
GET /userslist usersGET /users/{id}get one userPOST /userscreate a user
Good:
- All three use the same
UserResponsefor an individual user. GET /usersreturns{"items": [UserResponse], "total": ...}.GET /users/{id}returnsUserResponse.POST /usersreturnsUserResponsewith201 Created.
Bad:
GET /usersreturns{ "users": [ { "id": ..., "email": ... } ] }.GET /users/{id}returns{ "user": { "userId": ..., "emailAddress": ... } }.POST /usersreturns{ "id": ..., "email": ..., "role": ... }directly at the top level.
Here, field names and shapes differ for no good reason.
Response Models and HTTP Status Codes
The same endpoint can return different models depending on the HTTP status code.
For example, GET /users/{id}:
200 OK: aUserResponseobject.404 Not Found: an error object.401 Unauthorized: a different error object.
Even if you do not define them as "models" in your language, conceptually they are models:
UserResponse:
id: integer
email: string
full_name: string
ErrorResponse:
detail: string
code: string (optional)Example responses:
Success 200:
{
"id": 10,
"email": "alice@example.com",
"full_name": "Alice Doe"
}Not found 404:
{
"detail": "User not found",
"code": "USER_NOT_FOUND"
}Having a standard error model is extremely helpful. For example, you can agree that all errors look like:
{
"detail": "Human readable message",
"code": "MACHINE_READABLE_CODE",
"fields": {
"email": "Invalid email format"
}
}You will use this again when you design error responses for validation and failures.
Versioned Response Models
When you version your API (for example, /v1/users, /v2/users), you may need different response models for each version.
Example:
v1user response:
{
"id": 7,
"email": "user@example.com"
}v2user response:
{
"id": 7,
"email": "user@example.com",
"full_name": "User Example"
}You should treat each version's response as a separate model, even if they are similar.
Once you publish an API version, avoid removing or renaming fields in its response models. That breaks existing clients. Prefer adding new fields or introducing a new API version.
Common Patterns for Response Models
Here are some widely used patterns you will see in real APIs.
Resource Model
Represents a single resource, like a user, product, or order.
Example:
ProductResponse:
id: integer
name: string
price: number
currency: string
in_stock: boolean{
"id": 123,
"name": "Mechanical Keyboard",
"price": 99.99,
"currency": "USD",
"in_stock": true
}List + Pagination Model
For listing many resources with pagination:
PaginatedProductsResponse:
items: array of ProductResponse
total: integer
page: integer
page_size: integer{
"items": [
{ "id": 1, "name": "Keyboard", "price": 50, "currency": "USD", "in_stock": true },
{ "id": 2, "name": "Mouse", "price": 20, "currency": "USD", "in_stock": false }
],
"total": 42,
"page": 1,
"page_size": 20
}Nested Models
Resources often contain other resources.
Example, an order with items:
OrderItemResponse:
product_id: integer
product_name: string
quantity: integer
unit_price: number
OrderResponse:
id: integer
user_id: integer
items: array of OrderItemResponse
total_amount: number
currency: string
created_at: datetime{
"id": 1001,
"user_id": 7,
"items": [
{
"product_id": 123,
"product_name": "Mechanical Keyboard",
"quantity": 1,
"unit_price": 99.99
},
{
"product_id": 456,
"product_name": "Gaming Mouse",
"quantity": 2,
"unit_price": 35.50
}
],
"total_amount": 170.99,
"currency": "USD",
"created_at": "2026-02-01T10:00:00Z"
}Minimal Models for Some Endpoints
Some operations do not need to return the full resource.
For example, DELETE /tasks/{id} might return:
- No body, just status
204 No Content, or - A simple confirmation model:
DeleteResponse:
success: boolean{
"success": true
}Design this intentionally. Do not return huge objects when the client does not need them.
Transforming Internal Data to Response Models
Inside your backend, you might have:
- Database models (like ORM models).
- Domain models (business objects).
- Raw query results.
You usually transform them into response models before sending them to the client.
Conceptually, this is a mapping:
DatabaseUser(id, email, password_hash, is_deleted, created_at, updated_at)
-> UserResponse(id, email, created_at)Often you will:
- Exclude sensitive or irrelevant fields.
- Rename fields to match API conventions (like
created_at). - Compute extra fields such as
full_namefromfirst_name + last_name. - Format dates and times into ISO 8601 strings.
Example transformation idea:
db_user = {
"id": 10,
"email": "alice@example.com",
"password_hash": "...",
"first_name": "Alice",
"last_name": "Doe",
"created_at": 1690000000 // unix timestamp
}
response_user = {
"id": 10,
"email": "alice@example.com",
"full_name": "Alice Doe",
"created_at": "2023-07-22T10:13:20Z"
}The response model defines the second structure, not the first.
Designing Response Models: Practical Guidelines
You will often design response models while you design your endpoints and REST resources.
Here are practical rules.
1. Keep them minimal but useful
Include:
- Identifiers (
id). - Key attributes that the client must display or use.
- Timestamps when they are relevant.
- Status fields that help the client know what to do next.
Avoid:
- Internal flags (
is_deleted,db_version). - Whole related objects when the client only needs an
id. - Entire linked resources if it creates huge, deeply nested responses on every request.
2. Use consistent naming
Pick one style and stick with it. Examples:
snake_casefor all JSON fields:created_at,full_name.camelCasefor all JSON fields:createdAt,fullName.
Mixing them is confusing.
3. Think about future changes
You can safely:
- Add new optional fields to responses. Old clients will ignore them.
- Add new endpoints.
You should avoid:
- Renaming or removing existing fields in a published API version.
- Changing a field's type, for example changing from string to integer.
If you must make breaking changes, use a new API version (/v2/...) with new response models.
4. Standardize error responses
Instead of many different shapes, have a shared error structure.
For example, agree that every error returns:
{
"detail": "Message",
"code": "SOME_CODE",
"fields": {
"field_name": "Field-specific message"
}
}Then your "error response model" is clear and consistent.
Example: Response Models in a Task Management API
Imagine a simple Task Management API. Here is how you might define its response models.
TaskResponse
TaskResponse:
id: integer
title: string
description: string (optional)
completed: boolean
created_at: datetime
updated_at: datetime
GET /tasks/1 might return:
{
"id": 1,
"title": "Write documentation",
"description": "Write docs for the new API",
"completed": false,
"created_at": "2026-02-01T10:00:00Z",
"updated_at": "2026-02-01T10:05:00Z"
}TaskListResponse
TaskListResponse:
items: array of TaskResponse
total: integer
page: integer
page_size: integer
GET /tasks?page=1&page_size=2:
{
"items": [
{
"id": 1,
"title": "Write documentation",
"description": "Write docs for the new API",
"completed": false,
"created_at": "2026-02-01T10:00:00Z",
"updated_at": "2026-02-01T10:05:00Z"
},
{
"id": 2,
"title": "Fix bug #123",
"description": null,
"completed": true,
"created_at": "2026-02-01T11:00:00Z",
"updated_at": "2026-02-01T11:30:00Z"
}
],
"total": 10,
"page": 1,
"page_size": 2
}ErrorResponse
ErrorResponse:
detail: string
code: string
fields: map<string, string> (optional)If validation fails on task creation:
{
"detail": "Validation error",
"code": "VALIDATION_ERROR",
"fields": {
"title": "Title is required"
}
}Why this design works
- The shape of a
TaskResponseis always the same. - Listing, retrieving, creating, and updating tasks all reuse
TaskResponse. - Errors all follow a common error model.
- Lists use a standard paginated structure.
This is exactly what you want from response models: clarity, consistency, and safety.
Summary
- A response model defines the shape and content of what your API returns.
- It is often different from the request model, especially to avoid exposing sensitive fields.
- Good response models:
- Are consistent across endpoints.
- Hide internal details and sensitive data.
- Provide structures for single resources, lists, and errors.
- Change slowly, and in backward compatible ways.
- You transform internal data into response models before sending responses to clients.
In later chapters, when you use frameworks like FastAPI and tools like OpenAPI, you will see how response models are declared in code and how they feed into automatic documentation and validation.
Views: 7
KAHIBARO