KAHIBARO
Discord Login Register

7.12. Response Models

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:

Response models are important because they:

In code, a response model is usually a class or type that describes the shape of the data you return.

Example idea (language agnostic):

text
UserResponse:
  id: integer
  email: string
  full_name: string
  created_at: datetime

This 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:

They are usually not identical.

Typical differences:

ConcernRequest Model (Input)Response Model (Output)
Sensitive dataMay contain raw password, token, etc.Must never include raw password or secrets
Server-generated fieldsClient does not send id, timestampsAPI usually returns id, created_at, etc.
Optional fieldsSome fields optional on creationSome fields always present after creation
Computed/derived fieldsUsually not sent by clientMay include extra computed info (e.g. age)

Example conceptual models:

text
UserCreateRequest:
  email: string
  password: string
  full_name: string (optional)
UserResponse:
  id: integer
  email: string
  full_name: string
  created_at: datetime

Key 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:

json
{
  "id": 42,
  "title": "Learn Backend",
  "completed": false
}

This corresponds to a response model like:

text
TodoResponse:
  id: integer
  title: string
  completed: boolean

Wrapped Object

Some teams always wrap the main data in a top-level field, often data:

json
{
  "data": {
    "id": 42,
    "title": "Learn Backend",
    "completed": false
  }
}

Why do this?

For example:

json
{
  "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:

json
[
  { "id": 1, "title": "Task 1", "completed": false },
  { "id": 2, "title": "Task 2", "completed": true }
]

Option 2, wrapped with metadata:

json
{
  "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:

json
{
  "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:

A safe response model:

text
UserResponse:
  id: integer
  email: string
  role: string
  created_at: datetime

Example JSON response:

json
{
  "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:

json
{
  "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:

Good:

Bad:

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}:

Even if you do not define them as "models" in your language, conceptually they are models:

text
UserResponse:
  id: integer
  email: string
  full_name: string
ErrorResponse:
  detail: string
  code: string (optional)

Example responses:

Success 200:

json
{
  "id": 10,
  "email": "alice@example.com",
  "full_name": "Alice Doe"
}

Not found 404:

json
{
  "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:

json
{
  "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:

json
{
  "id": 7,
  "email": "user@example.com"
}
json
{
  "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:

text
ProductResponse:
  id: integer
  name: string
  price: number
  currency: string
  in_stock: boolean
json
{
  "id": 123,
  "name": "Mechanical Keyboard",
  "price": 99.99,
  "currency": "USD",
  "in_stock": true
}

List + Pagination Model

For listing many resources with pagination:

text
PaginatedProductsResponse:
  items: array of ProductResponse
  total: integer
  page: integer
  page_size: integer
json
{
  "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:

text
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
json
{
  "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:

text
DeleteResponse:
  success: boolean
json
{
  "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:

You usually transform them into response models before sending them to the client.

Conceptually, this is a mapping:

text
DatabaseUser(id, email, password_hash, is_deleted, created_at, updated_at)
  -> UserResponse(id, email, created_at)

Often you will:

Example transformation idea:

text
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:

Avoid:

2. Use consistent naming

Pick one style and stick with it. Examples:

Mixing them is confusing.

3. Think about future changes

You can safely:

You should avoid:

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:

json
{
  "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

text
TaskResponse:
  id: integer
  title: string
  description: string (optional)
  completed: boolean
  created_at: datetime
  updated_at: datetime

GET /tasks/1 might return:

json
{
  "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

text
TaskListResponse:
  items: array of TaskResponse
  total: integer
  page: integer
  page_size: integer

GET /tasks?page=1&page_size=2:

json
{
  "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

text
ErrorResponse:
  detail: string
  code: string
  fields: map<string, string> (optional)

If validation fails on task creation:

json
{
  "detail": "Validation error",
  "code": "VALIDATION_ERROR",
  "fields": {
    "title": "Title is required"
  }
}

Why this design works

This is exactly what you want from response models: clarity, consistency, and safety.


Summary

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

Comments

Please login to add a comment.

Don't have an account? Register now!