# Unit 8: Request Body

## Introduction

In FastAPI, the request body is typically used to send data from a client to your API. It is often used for creating or updating resources. FastAPI automatically parses the request body based on the type hints you provide in your function parameters.

FastAPI uses Pydantic models to validate incoming data, ensuring that the request body matches the expected structure and data types. If the validation fails, FastAPI automatically returns a `422 Unprocessable Entity` response detailing the validation errors. This greatly simplifies error handling and ensures that your function only processes valid data.

***

## Request Body

### Demo 1

Let's implement a simple example of how to define an endpoint that expects a request body in FastAPI:

{% code lineNumbers="true" %}

```python
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    is_offer: bool = None

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    return item
```

{% endcode %}

We first declare a data model as a class `Item` that inherits from `BaseModel`. You should note that the `is_offer` is an optional attribute which has a default value is `None`. The function `create_item` receive a JSON object from a client  and then represented as a `dict` object in your program.

Since we used Python Type Hints and Pydantic models in the program, we already have data validation. If the data is invalid, the program will return a clear and informative error, indicating exactly where and what the incorrect data is.

To evaluate the program, we use [Postman software](https://www.postman.com/downloads/) and create a new `POST` request to the API endpoint at `http://127.0.0.1:8000/items/`, as shown in Fig. 1.

<figure><img src="https://4188609209-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fhp4eambztJcKKLa7ysKG%2Fuploads%2FKhQWw5Cd6M5SAIkj4fE1%2FScreenshot%202024-02-10%20at%2011.36.38%20AM.png?alt=media&#x26;token=835dc79c-f8f2-4e13-a6f6-454af8622cc2" alt=""><figcaption><p>Fig. 1. Create a new POST request in Postman software</p></figcaption></figure>

### Demo 2

In this second demonstration program, we will modify the data received from a client and subsequently return the modified data to the client.

{% code lineNumbers="true" %}

```python
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    tax: float = 0.0  # Default value for tax

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    item_dict = item.model_dump()
    if item.tax:
        final_price = item.price + item.tax
        item_dict.update({"final_price": final_price})
    return item_dict
```

{% endcode %}

{% hint style="warning" %}
The method `item.dict()` in class `BaseModel` is deprecated, use `item.model_dump()` instead.
{% endhint %}

We also use Postman software to evaluate the program, as shown in Fig. 2.

<figure><img src="https://4188609209-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fhp4eambztJcKKLa7ysKG%2Fuploads%2Fycz8fv0rEEsxJ1l3OXz1%2FScreenshot%202024-02-10%20at%2011.51.21%20AM.png?alt=media&#x26;token=e48655e1-5bc4-4484-b62b-79211dde2c53" alt=""><figcaption><p>Fig. 2. Create a new POST request in Postman software</p></figcaption></figure>

We can further analyze the program's error scenarios, including cases where the `tax` is entered as a textual value or when required attributes are missing.

```json
{
    "name": "beer",
    "price": 50000,
    "tax": "five thousand"
}
```

With the above request body, you will get the following error:

```json
{
    "detail": [
        {
            "type": "float_parsing",
            "loc": [
                "body",
                "tax"
            ],
            "msg": "Input should be a valid number, unable to parse string as a number",
            "input": "five thousand",
            "url": "https://errors.pydantic.dev/2.6/v/float_parsing"
        }
    ]
}
```

However, you should note that the below request body is correct (`tax` value is represented as a string) :

```json
{
    "name": "beer",
    "price": 50000,
    "tax": "5000"
}
```

### Demo 3: A complex parameter

In this third demo, we combine all three request body, path parameter and query parameter.

{% code lineNumbers="true" %}

```python
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    tax: float = 0.0  # Default value for tax

app = FastAPI()

@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, query: str = None):
    output = {"item_id": item_id, **item.model_dump()}
    if query:
        output.update({"query": query})
    return output
```

{% endcode %}

{% hint style="warning" %}
The method `item.dict()` in class `BaseModel` is deprecated, use `item.model_dump()` instead.
{% endhint %}

To evaluate the program, you should note that:

* The `update_item` function now accepts a `PUT` method, instead of the `POST` method as in the previous two programs.
* The `item_id` is a path parameter.
* The `query` is a query parameter.
* The `name`, `tax`, and `price` are attributes of a request body.
* The URL to evaluate this API endpoint is `http://127.0.0.1:8000/items/1001?query=update-item-price`

Fig. 3 and Fig. 4 shows a demonstration of this API endpoint in Postman software.

<figure><img src="https://4188609209-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fhp4eambztJcKKLa7ysKG%2Fuploads%2FxRTEFTckX2XSyiYlkrET%2FScreenshot%202024-02-10%20at%2012.23.03%20PM.png?alt=media&#x26;token=c71f3517-d27f-4f2d-96f1-37d21c3dd5ab" alt=""><figcaption><p>Fig. 3. Adding Query Parameter in Postman software</p></figcaption></figure>

<figure><img src="https://4188609209-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fhp4eambztJcKKLa7ysKG%2Fuploads%2FfDQ5UDfvWscHx7W6ka0v%2FScreenshot%202024-02-10%20at%2012.23.11%20PM.png?alt=media&#x26;token=844bfc5b-f1a5-4a62-9b2a-b74285b1b177" alt=""><figcaption><p>Fig. 4. Create a new PUT request in Postman software</p></figcaption></figure>

***

## Summary

FastAPI provides a powerful and intuitive way to handle request bodies through its seamless integration with Pydantic models. This combination not only ensures that your API endpoints can efficiently parse and validate incoming data but also significantly enhances the developer experience by reducing boilerplate code and automating documentation. By leveraging type hints and Pydantic models, developers can define clear, concise, and self-documenting APIs that enforce data integrity and provide meaningful error messages when data validation fails.

***

## Exercises

**1. Create a Student (Body) with Class Path**

**Scenario:** Add a new student to a specific class.

* **Endpoint:** `POST /classes/{class_id}/students`
* **Path params:**
  * `class_id: int`
* **Query params (optional):**
  * `notify: bool = false` — send a welcome email or not
* **Request body (JSON):**

  ```json
  {
      "full_name": "string",
      "email": "string",
      "year": 1
  }
  ```
* **Tasks:**
  1. Define a Pydantic model `StudentCreate`.
  2. Validate `email` format and constrain `year` to `1..4`.
  3. Return `201` with created student plus the `class_id` and `notify` flag.
* **Example response:**

  ```json
  {
      "class_id": 101,
      "student": {
          "id": 7,
          "full_name": "Jane Doe",
          "email": "jane@stu.tdtu.edu.vn",
          "year": 1
      },
      "notified": false
  }
  ```

***

**2. Bulk Insert with Filtering Preview**

**Scenario:** Upload multiple products to a store, with a **preview** mode.

* **Endpoint:** `POST /stores/{store_id}/products:bulk`
* **Path params:**
  * `store_id: int`
* **Query params:**
  * `dry_run: bool = true` — A dry-run means simulating the action without actually performing it. If true, validate and show what would happen without saving.
  * `category: str | None` — filter the input to only persist products in this category (when not `dry_run`)
* **Example URL:**
  * `POST /stores/101/products:bulk?dry_run=true`
  * `POST /stores/101/products:bulk?dry_run=false&category=electronics`
* **Request body (JSON array):**

  ```json
  [
    { "sku": "ABC-1", "name": "Laptop", "price": 1200.0, "category": "electronics" },
    { "sku": "ABC-2", "name": "Mouse",  "price": 25.0,   "category": "accessories" }
  ]
  ```
* **Tasks:**
  1. Define `ProductIn` model with `price > 0`.
  2. If `dry_run=true`, return `{"valid": true|false, "errors": [...], "count": N}`.
  3. If `dry_run=false`, save only items matching `category` (if provided) and return the persisted items.
* **Example dry-run response:**&#x20;

  ```json
  { "valid": true, "errors": [], "count": 2 }
  ```

***

**3. Partial Update (PATCH) with Field Mask**

{% hint style="info" %}
In HTTP methods, **`PATCH`** is used to **partially update a resource**. Unlike `PUT`, which requires resubmitting the entire resource, `PATCH` allows updating only specific fields. In real-world systems (such as e-commerce orders), this is useful when changing delivery notes without altering the status or address, or when updating just the order status (e.g., from *processing* to *shipped*). This approach is both **more efficient** and **safer**, as it avoids unnecessary overwriting of unchanged data.
{% endhint %}

**Scenario:** Partially update an order belonging to a user.

* **Endpoint:**
  * `PATCH /users/{user_id}/orders/{order_id}`
* **Path params:**
  * `user_id: int`, `order_id: int`
* **Query params (optional):**
  * `fields: str | None` — comma-separated list of fields that are allowed to be updated (e.g., `status,shipping_address`)
* **Example URL:**
  * `PATCH /users/55/orders/2001?fields=notes,shipping_address`
* **Request body (JSON, all optional fields):**

  ```json
  {
    "status": "processing|shipped|delivered|cancelled",
    "shipping_address": { "street": "string", "city": "string", "zip": "string" },
    "notes": "string | null"
  }
  ```
* **Tasks:**
  1. Create `OrderPatch` with all fields optional.
  2. If `fields` is provided, only apply updates to those fields; reject others with `422` and a clear message.
  3. Enforce valid status transitions (e.g., cannot go from `delivered` → `processing`).
* **Example response:**

  ```json
  {
      "order_id": 33,
      "updated_fields": [
          "status",
          "notes"
      ],
      "order": {
          "status": "shipped",
          "shipping_address": {
              "street": "19, Nguyen Huu Tho, P. Tan Hung",
              "city": "Ho Chi Minh",
              "zip": "70000"
          },
          "notes": "Leave at door"
      }
  }
  ```

***

**4. Search + Complex Body Filters + Pagination**

{% hint style="info" %}
`limit` tells the API how many results to return at most, and `offset` tells the API how many results to skip before starting to return data.

We use `POST` in this exercise, instead of `GET`, because:

* The filters are too complex for query params.
* It's more scalable and maintainable.
* It avoids URL length limits.
  {% endhint %}

**Scenario:** Search tickets for a given project using complex criteria + pagination.

* **Endpoint:** `POST /projects/{project_id}/tickets:search`
* **Path params:**
  * `project_id: str`
* **Query params:**
  * `limit: int = 10`, `offset: int = 0`, `sort: str = "created_at:desc"`
* **Example URL:**
  * Returns tickets 6—10 assigned to user `u42`, sorted by priority ascending: `POST /projects/alpha123/tickets:search?limit=5&offset=5&sort=priority:asc`
* **Request body (JSON filters):**

  ```json
  {
      "assignees": [
          "u123",
          "u456"
      ],
      "status": [
          "open",
          "in_progress"
      ],
      "created_range": {
          "from": "2025-09-01",
          "to": "2025-09-30"
      },
      "labels": [
          "backend",
          "bug"
      ],
      "text": "database timeout"
  }
  ```
* **Tasks:**
  1. Define nested filter models (`DateRange`, `TicketSearch`).
  2. Validate `limit in 1..100` and `sort` format `<field>:<asc|desc>`.
  3. Apply filters from body, then paginate + sort via query params.
  4. Return `total`, `items`, and `next_offset` if more results exist.
* **Example response:**

  ```json
  {
      "total": 57,
      "items": [
          {
              "id": 901,
              "title": "Fix DB pool",
              "status": "open"
          }
      ],
      "next_offset": 10
  }
  ```

***

**5\*. Idempotent Payments with Idempotency-Key**

{% hint style="info" %}
An **Idempotency-Key** is a unique identifier (often a UUID) that the client sends with a request to tell the server: "If you receive this same request again with the same key, don’t perform the action twice — just return the same result you gave the first time."

The key benefits of using an Idempotency-Key are that it prevents duplicate operations (such as accidental double-charging), makes APIs more reliable in unstable networks, and improves user experience by removing the risk of unintended repeated actions like pressing "Pay" twice.
{% endhint %}

**Scenario:** Process a payment for an invoice with **idempotency** and **mode toggles**.

* **Endpoint:** `POST /invoices/{invoice_id}/payments`
* **Path params:**
  * `invoice_id: str`
* **Query params:**
  * `mode: str = "live"` — `"test"` or `"live"` controls which ledger to hit
  * `retry_on_timeout: bool = true` — allow internal retry on gateway timeouts
* **Headers to read (simulate in code):**
  * `Idempotency-Key: <uuid>` — identical requests with the same key must return the same result without double-charging
* **Request body (JSON):**

  ```json
  {
    "method": "card|bank_transfer|wallet",
    "amount": 125.50,
    "currency": "USD",
    "customer": {
      "id": "cus_123",
      "email": "alice@example.com"
    },
    "metadata": { "order_id": "ORD-8891" }
  }
  ```
* **Tasks:**
  1. Validate `amount > 0`, supported `currency`, and required fields per `method`.
  2. Respect `mode` query param to select storage bucket (e.g., in-memory dicts `payments_test` vs `payments_live`).
  3. Implement **idempotency**: if the same `Idempotency-Key` is seen for the same `invoice_id`, return the previously stored response.
  4. If `retry_on_timeout=true`, simulate a transient gateway timeout and an internal retry strategy.
  5. Return normalized payment result with status: `succeeded|failed|pending`, and echo `idempotency_key`.
* **Example success response:**

  ```json
  {
    "invoice_id": "INV-1001",
    "payment_id": "pay_77abc",
    "status": "succeeded",
    "amount": 125000.0,
    "currency": "VND",
    "mode": "live",
    "idempotency_key": "8f1a6a57-6e2c-4b7b-8c59-3b5b2b21a1a2",
    "metadata": { "order_id": "ORD-8891" }
  }
  ```
