# FastAPI: Path Params, Query, Body and HTTPException<no value>

## What will we learn?

In the previous lesson the functions received no input at all. Now they will receive the three kinds we learned:

| Input | How FastAPI knows |
|---|---|
| **Path Parameter** | A name in curly brackets in the path: `"/students/{student_id}"` |
| **Query Parameter** | A function parameter that does not appear in the path |
| **Body** | A parameter whose type is a **Pydantic model** |

And in addition: **Type Hints**, **`status_code`**, **`HTTPException`**, and the automatic **422** error.

## Why do we need this?

Without input, a server can only always return the same thing. With input, it can return **a specific student**, **filter**, and **create** new students. And a huge bonus: FastAPI **checks and translates the input for us**. A hand-written calculator server crashed on `"hello"`. Here, wrong input will automatically get a **422** with an exact explanation, and the server will keep working.

## Explanation from the basics

### Type Hints

**Type Hint** = a note in the code that says what the **expected type** of a parameter is:

```python
def get_student(student_id: int):
```

`student_id: int` = "this parameter is supposed to be an `int`". In regular Python, Type Hints **are not enforced**: they are only documentation, which helps VS Code and developers. **But FastAPI reads them and uses them**:

1. **Conversion** — the string `"7"` arrives from the URL, and FastAPI converts it to `7`, because we asked for `int`.
2. **Validation** — if `"abc"` arrived, FastAPI does **not** call the function, but returns a **422** with an explanation.
3. **Documentation** — `/docs` shows the type of every parameter.

### Path Parameter

```python
@app.get("/students/{student_id}")
def get_student(student_id: int):
```

`{student_id}` in the path = "this part changes". FastAPI passes it to the parameter with **exactly the same name**.

### Query Parameters

**Every function parameter that does not appear in the path (and is not a model) = a Query Parameter.**

```python
def list_students(city: str | None = None, min_age: int = 0, limit: int = 10):
```

| Definition | Meaning |
|---|---|
| `min_age: int = 0` | **Optional**, with a default of 0. |
| `limit: int` (without `=`) | **Required**. Without it → 422. |
| `city: str \| None = None` | **Optional, without a default value**: "either a string, or `None`". `None` = "it was not sent, so we don't filter". |

`str | None` is read "str **or** None". It is a Type Hint that says two types are allowed.

### Body with Pydantic

**Pydantic** is a library (installed together with FastAPI) that defines **a structure of data** with types. You create a **Class** that inherits from `BaseModel` (Inheritance, the OOP topic!):

```python
from pydantic import BaseModel

class StudentIn(BaseModel):
    name: str
    age: int
    city: str = "Unknown"
```

- Every line = **a field**, with a type. A field with `=` is optional, with a default.
- **There is no `__init__`!** `BaseModel` builds it by itself from the fields.
- When a function's parameter is of this type, FastAPI understands that it is a **Body**: it reads the JSON, checks that all the fields exist and are of the right type, and builds an object. You access the fields with a dot: `student.name`.

{{< box "analogy" "Analogy: a form with required fields" >}}
A Pydantic model is **a printed form**: "Name (required, text). Age (required, a number). City (optional)". The clerk (FastAPI) does not pass on for handling (to your function) a form that is missing a required field, or that has "twenty" written in the age field. They immediately return it to the person who submitted it (**422**) with the problematic field marked. Your function receives **only valid forms**.
{{< /box >}}

### `status_code` and `HTTPException`

- **`@app.post("/students", status_code=201)`** — the success code of this Endpoint is **201 Created**, and not 200.
- **`raise HTTPException(status_code=404, detail="...")`** — stops the function and returns an error code to the Client with the JSON `{"detail": "..."}`. It is a regular `raise` (remember the exceptions topic!), and FastAPI catches it and turns it into an HTTP response.

## Syntax

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

app = FastAPI()

class ItemIn(BaseModel):              # body model
    name: str
    price: float
    in_stock: bool = True

@app.get("/items/{item_id}")          # path parameter
def get_item(item_id: int):
    ...
    raise HTTPException(status_code=404, detail="item not found")

@app.get("/items")                    # query parameters
def list_items(max_price: float | None = None, limit: int = 10):
    ...

@app.post("/items", status_code=201)  # body
def create_item(item: ItemIn):
    item.name, item.price             # use the validated fields
```

## First code example

**A students API in memory** (in RAM. There is no Database yet, it will come in the databases topic). `week-4/hello-api/students_memory.py`:

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

app = FastAPI(title="Students (in memory)")

students = [
    {"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
    {"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
    {"id": 3, "name": "Noa", "age": 22, "city": "Haifa"},
]

class StudentIn(BaseModel):
    name: str
    age: int
    city: str = "Unknown"

@app.get("/students")
def list_students(city: str | None = None, min_age: int = 0, limit: int = 10):
    result = []
    for student in students:
        if city is not None and student["city"] != city:
            continue
        if student["age"] < min_age:
            continue
        result.append(student)
    return result[:limit]

@app.get("/students/{student_id}")
def get_student(student_id: int):
    for student in students:
        if student["id"] == student_id:
            return student
    raise HTTPException(status_code=404, detail="student " + str(student_id) + " not found")

@app.post("/students", status_code=201)
def create_student(student: StudentIn):
    new_id = max(s["id"] for s in students) + 1
    new_student = {"id": new_id, "name": student.name, "age": student.age, "city": student.city}
    students.append(new_student)
    return new_student
```

**Running:** `uvicorn students_memory:app --reload` (notice: the file's name changed!).

## Breaking down the code line by line

| Code | Explanation |
|---|---|
| `students = [...]` | A temporary "Database" in RAM: a list of dicts. **It is deleted on every restart** (remember RAM from the computer topic!). In the databases topic we will move to a real Database. |
| `class StudentIn(BaseModel):` | The Body model: what the Client **sends** on creation. There is no `id` in it, because the server sets the id. |
| `city: str = "Unknown"` | An optional field in the Body. |
| `list_students(city: str \| None = None, min_age: int = 0, limit: int = 10)` | Three **Query Parameters**, all optional. `/students?city=Haifa&min_age=23`. |
| `if city is not None and ...: continue` | Filtering only if `city` was sent. `continue` skips to the next student. |
| `return result[:limit]` | Slicing: at most `limit` results. |
| `@app.get("/students/{student_id}")` + `student_id: int` | A **Path Parameter**, with conversion to `int`. |
| `raise HTTPException(status_code=404, ...)` | Not found, so a 404 with a message. |
| `@app.post(..., status_code=201)` | Creating returns **201**. |
| `create_student(student: StudentIn)` | The type is a model, so it is a **Body**. FastAPI decodes and checks it. |
| `student.name`, `student.age` | Accessing the fields of an object (the OOP topic). |
| `max(s["id"] for s in students) + 1` | A new identifier: the largest + 1. |

## Another example

**A full test of all the kinds of input, and the real responses.** (You can also test in `/docs`, or with curl.)

| Request | Code | Response |
|---|---|---|
| `GET /students` | 200 | All three |
| `GET /students?city=Haifa` | 200 | Dana and Noa |
| `GET /students?min_age=23&limit=1` | 200 | `[{"id": 1, "name": "Dana", ...}]` |
| `GET /students/2` | 200 | `{"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"}` |
| `GET /students/42` | **404** | `{"detail": "student 42 not found"}` |
| `GET /students/abc` | **422** | See below |
| `GET /students?min_age=old` | **422** | See below |
| `POST /students` + `{"name": "Tom", "age": 27}` | **201** | `{"id": 4, "name": "Tom", "age": 27, "city": "Unknown"}` |
| `POST /students` + `{"name": "Rina", "age": "twenty"}` | **422** | See below |

**The 422 error — what it looks like** (`GET /students/abc`):

```json
{
  "detail": [
    {
      "type": "int_parsing",
      "loc": ["path", "student_id"],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "abc"
    }
  ]
}
```

**Reading a 422:**

- `loc` (Location) — **where** the problem is: `["path", "student_id"]`. In the Query it would be `["query", "min_age"]`, and in the Body `["body", "age"]`.
- `msg` — **what** the problem is.
- `input` — **what** was actually received.
- **Our function did not run at all.** FastAPI stopped the request before that. There is no need for `try`/`except` around `int()`.

**And after Tom's `POST`:** `GET /students?city=Unknown` returns Tom, because the city got the default. But **stop the server and start it again**: Tom **disappeared**. The data was in RAM. That is exactly the reason for a Database (coming next!).

## Common mistake

**Mistake 1: a name that does not match between the path and the parameter.**

```python
@app.get("/students/{student_id}")
def get_student(id: int):
```

FastAPI does not find a parameter named `student_id`, and treats `id` as a **required Query Parameter**. Every request to `/students/7` will return a 422 (`"loc": ["query", "id"]`, missing). **The name in the curly brackets must be identical to the parameter's name.**

**Mistake 2: a Path Parameter without a Type Hint.**

```python
def get_student(student_id):
    for student in students:
        if student["id"] == student_id:
```

Without `: int`, the parameter arrives as the **string** `"2"`, and `2 == "2"` is `False`. The result: always 404, without any error. **A silent Logical Error.**

**Mistake 3: the order of Routes with a fixed path and a path with a parameter.**

```python
@app.get("/students/{student_id}")
def get_student(student_id: int): ...

@app.get("/students/top")
def top_students(): ...
```

`GET /students/top` → **422**! FastAPI checks Routes **in order**, and `/students/{student_id}` "catches" `top` as the student_id, and fails to convert it to an int. **Fixed paths before paths with parameters.**

**Mistake 4: `return` instead of `raise` for an HTTPException.**

```python
return HTTPException(status_code=404, detail="not found")
```

Returns **200**, with a strange object inside the JSON. An error is **raised** (`raise`), not returned.

**Mistake 5: including `id` in the Body's model.** If `StudentIn` includes `id: int`, the Client **must** send an id, and can send an id that already exists. **The server** sets the identifier. The Body's model contains only what the Client is supposed to send.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

### Exercise 1: A Path Parameter with a Type Hint

Write an API with a list of students in memory (dictionaries with `id`, `name`, `age`, `city`), and a Route `GET /students/{student_id}` that returns the student, or 404 with a `detail` of `"student not found"`.

1. What will happen on the request `GET /students/abc`?

{{< solution >}}
```python
from fastapi import FastAPI, HTTPException

app = FastAPI(title="Students API")

students = [
    {"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
    {"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
]


@app.get("/students/{student_id}")
def get_student(student_id: int):
    for student in students:
        if student["id"] == student_id:
            return student
    raise HTTPException(status_code=404, detail="student not found")
```

- `GET /students/2` → 200 with Avi. `GET /students/9` → **404** `{"detail": "student not found"}`.
- `GET /students/abc` → **422**: the Type Hint `student_id: int` makes FastAPI check and convert by itself. The function does not run at all.
{{< /solution >}}

### Exercise 2: Query Parameters

Add a Route `GET /students` that receives an optional Query Parameter `city` (default `None`), and returns only the students from that city — or all of them if no city was sent.

{{< solution >}}
```python
from fastapi import FastAPI

app = FastAPI(title="Students API")

students = [
    {"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
    {"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
]


@app.get("/students")
def list_students(city: str = None):
    if city is None:
        return students
    result = []
    for student in students:
        if student["city"] == city:
            result.append(student)
    return result
```

- `GET /students` → both students.
- `GET /students?city=Haifa` → only Dana.

A function parameter that is **not** part of the path automatically becomes a **Query Parameter**. A default value makes it optional.
{{< /solution >}}

### Exercise 3: POST with a Body

Add a Pydantic model named `StudentIn` (`name`, `age`, `city`) and a Route `POST /students` that returns 201, gives the student a new `id` and adds them to the list. An age below 16 → 400.

{{< solution >}}
```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="Students API")

MIN_AGE = 16

students = [
    {"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
]


class StudentIn(BaseModel):
    name: str
    age: int
    city: str


@app.post("/students", status_code=201)
def create_student(student: StudentIn):
    if student.age < MIN_AGE:
        raise HTTPException(status_code=400, detail="age must be at least 16")
    new_student = {"id": len(students) + 1, "name": student.name,
                   "age": student.age, "city": student.city}
    students.append(new_student)
    return new_student
```

- `POST /students` with `{"name": "Noa", "age": 22, "city": "Eilat"}` → **201** with `id: 2`.
- Age 12 → **400**. A body without `age` → **422** — Pydantic rejects a wrong structure before the function runs.
{{< /solution >}}

### Exercise 4: 422 versus 400

1. What is the difference between 422 and 400 in FastAPI? Who returns each one?
2. For each request — which code will come back?

```text
POST /students  {"name": "Noa", "age": "abc", "city": "Eilat"}
POST /students  {"name": "Noa", "age": 10, "city": "Eilat"}
POST /students  {"name": "Noa", "city": "Eilat"}
```

{{< solution >}}
1. **422** — **FastAPI and Pydantic** return it automatically when the structure or the type is wrong. **400** — **our code** returns it when the value is valid in terms of type, but does not meet a business rule.
2. The answers:
   - `"age": "abc"` → **422** (the age is not a number).
   - `"age": 10` → **400** (a valid number, but below the minimum age — our rule).
   - Without `age` → **422** (a required field is missing).
{{< /solution >}}

### Exercise 5: Statistics

Add a Route `GET /students/stats` that returns the number of students and the average age (rounded to one digit). Where should it be defined relative to `GET /students/{student_id}`, and why?

{{< solution >}}
```python
from fastapi import FastAPI, HTTPException

app = FastAPI(title="Students API")

students = [
    {"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
    {"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
]


@app.get("/students/stats")
def students_stats():
    total_age = 0
    for student in students:
        total_age = total_age + student["age"]
    return {"count": len(students), "average_age": round(total_age / len(students), 1)}


@app.get("/students/{student_id}")
def get_student(student_id: int):
    for student in students:
        if student["id"] == student_id:
            return student
    raise HTTPException(status_code=404, detail="student not found")
```

`GET /students/stats` → `{"count": 2, "average_age": 27.5}`.

Define `/students/stats` **before** `/students/{student_id}`. FastAPI checks Routes in order — if `{student_id}` came first, `stats` would be treated as an id, and fail with 422 because it is not a number.
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **Type Hints** (`x: int`) — FastAPI converts and checks according to them. Invalid input gets a **422** automatically, and the function does not run.
- **Path Parameter**: `"/students/{student_id}"` + `student_id: int`, with exactly the same name.
- **Query**: every parameter that is not in the path. `x: int = 0` is optional. `x: str | None = None` is optional without a default. Without `=`, required.
- **Body**: a Class that inherits from `BaseModel` (Pydantic). A parameter of this type = the JSON from the request, after validation.
- `status_code=201` for creating. **`raise HTTPException(404, ...)`** for not found, and 400 for a business rule.
- **Fixed paths before paths with a parameter.** And data in RAM is deleted on restart, which is why you need a Database.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [FastAPI — Path Parameters](https://fastapi.tiangolo.com/tutorial/path-params/)
- [FastAPI — Query Parameters](https://fastapi.tiangolo.com/tutorial/query-params/)
- [FastAPI — Request Body](https://fastapi.tiangolo.com/tutorial/body/)
- [FastAPI — Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/)

<!-- docs:end -->
