> ## Documentation Index
> Fetch the complete documentation index at: https://fastapi2day.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# In-Memory CRUD Operations with FastAPI and Pydantic

> Implement a fully working Employee Management System CRUD API using in-memory Python dictionaries to focus on FastAPI routing and response models.

Before connecting to a real database, building a CRUD API backed by in-memory Python dictionaries is the fastest way to master FastAPI's routing, validation, and response model patterns without any database overhead. In this lesson, you'll implement all five standard CRUD operations — list, retrieve, create, update, and delete — for an Employee Management System. Every pattern you learn here translates directly to database-backed APIs later.

***

## In-Memory Data Flow

Since you're not using a database yet, your application's state lives in the server's RAM. Incoming HTTP requests read from or write to a global Python dictionary:

```mermaid theme={null}
graph TD
    Client[Client HTTP Request] --> Route[FastAPI Endpoint Router]

    subgraph RAM [Server Memory]
        StateDict[Global EMPLOYEES Dict]
    end

    Route -->|GET| StateDict
    Route -->|POST / PUT / DELETE| StateDict
    StateDict -->|Returns updated/fetched record| Route
    Route --> Client
```

<Warning>
  Because this state lives in RAM, **restarting Uvicorn or triggering a hot-reload will wipe the in-memory state** and reset data to its initial mock values. This is expected behaviour at this stage — you'll replace it with database persistence in a later lesson.
</Warning>

***

## Setting Up the Pydantic Schemas

Good schema design separates incoming data (`EmployeeCreate`), partial updates (`EmployeeUpdate`), and the outgoing response (`EmployeeOut`):

```python theme={null}
from pydantic import BaseModel, Field

class EmployeeBase(BaseModel):
    name: str = Field(..., min_length=2, max_length=50, examples=["Jane Doe"])
    department: str = Field(..., examples=["Engineering"])
    role: str = Field(..., examples=["Software Engineer"])

class EmployeeCreate(EmployeeBase):
    pass  # Inherits all fields from EmployeeBase

class EmployeeUpdate(BaseModel):
    # All fields are optional for partial updates
    name: str | None = Field(None, min_length=2, max_length=50)
    department: str | None = Field(None)
    role: str | None = Field(None)

class EmployeeOut(EmployeeBase):
    id: int  # Added by the server, not sent by the client
```

***

## The Complete CRUD Implementation

Create a file named `ems_app.py` in your project folder:

```python theme={null}
# ems_app.py
from fastapi import FastAPI, HTTPException, status, Query
from pydantic import BaseModel, Field

app = FastAPI(
    title="Employee Management System (In-Memory)",
    description="Foundational CRUD API for managing employees.",
    version="1.0.0"
)

# ----------------- PYDANTIC SCHEMAS -----------------

class EmployeeBase(BaseModel):
    name: str = Field(..., min_length=2, max_length=50, examples=["Jane Doe"])
    department: str = Field(..., examples=["Engineering"])
    role: str = Field(..., examples=["Software Engineer"])

class EmployeeCreate(EmployeeBase):
    pass

class EmployeeUpdate(BaseModel):
    name: str | None = Field(None, min_length=2, max_length=50)
    department: str | None = Field(None)
    role: str | None = Field(None)

class EmployeeOut(EmployeeBase):
    id: int

# ----------------- MOCK DATA STATE -----------------

EMPLOYEES: dict[int, dict] = {
    1: {"id": 1, "name": "Alice Smith", "department": "Engineering", "role": "Backend Developer"},
    2: {"id": 2, "name": "Bob Jones", "department": "Product", "role": "Product Manager"}
}

# ----------------- CRUD ENDPOINTS -----------------

# 1. READ ALL — Retrieve a list of employees
@app.get("/employees", response_model=list[EmployeeOut])
def list_employees(
    department: str | None = Query(None, description="Filter by department name"),
    skip: int = Query(0, ge=0),
    limit: int = Query(10, ge=1, le=100)
):
    results = list(EMPLOYEES.values())

    if department:
        results = [
            emp for emp in results
            if emp["department"].lower() == department.lower()
        ]

    return results[skip : skip + limit]


# 2. READ ONE — Retrieve a single employee by ID
@app.get("/employees/{employee_id}", response_model=EmployeeOut)
def get_employee(employee_id: int):
    employee = EMPLOYEES.get(employee_id)
    if not employee:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Employee with ID {employee_id} not found."
        )
    return employee


# 3. CREATE — Onboard a new employee
@app.post("/employees", response_model=EmployeeOut, status_code=status.HTTP_201_CREATED)
def create_employee(employee: EmployeeCreate):
    new_id = max(EMPLOYEES.keys()) + 1 if EMPLOYEES else 1
    new_employee = {"id": new_id, **employee.model_dump()}
    EMPLOYEES[new_id] = new_employee
    return new_employee


# 4. UPDATE — Modify an existing employee's details
@app.put("/employees/{employee_id}", response_model=EmployeeOut)
def update_employee(employee_id: int, employee_update: EmployeeUpdate):
    if employee_id not in EMPLOYEES:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Employee with ID {employee_id} not found."
        )

    stored_employee = EMPLOYEES[employee_id]
    # Update only fields that were actually sent in the request
    update_data = employee_update.model_dump(exclude_unset=True)

    for key, value in update_data.items():
        stored_employee[key] = value

    EMPLOYEES[employee_id] = stored_employee
    return stored_employee


# 5. DELETE — Remove an employee
@app.delete("/employees/{employee_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_employee(employee_id: int):
    if employee_id not in EMPLOYEES:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Employee with ID {employee_id} not found."
        )
    del EMPLOYEES[employee_id]
    # HTTP 204 requires no response body
```

***

## Running and Testing the API

<Steps>
  <Step title="Start the server">
    ```bash theme={null}
    uvicorn ems_app:app --reload
    ```
  </Step>

  <Step title="Open Swagger UI">
    Navigate to `http://127.0.0.1:8000/docs` in your browser to see all five endpoints listed and interactive.
  </Step>

  <Step title="Test the full CRUD lifecycle">
    1. **POST** `/employees` — add a new employee. You should receive a `201 Created` response with the new record and its assigned `id`.
    2. **GET** `/employees` — verify the new employee appears in the list.
    3. **GET** `/employees/1` — retrieve a single employee by ID.
    4. **PUT** `/employees/1` — update the employee's department. Only send the field(s) you want to change.
    5. **DELETE** `/employees/2` — remove an employee. You should receive a `204 No Content` with no body.
  </Step>
</Steps>

***

## Key Patterns to Remember

| Pattern                     | Code                                     | Why It Matters                                                                                               |
| --------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `exclude_unset=True`        | `model_dump(exclude_unset=True)`         | Only updates fields the client actually sent — prevents accidentally overwriting existing values with `None` |
| Response model filtering    | `response_model=EmployeeOut`             | Ensures only declared fields are returned — never accidentally expose internal data                          |
| `HTTP_204_NO_CONTENT`       | `status_code=status.HTTP_204_NO_CONTENT` | Delete endpoints must return no body — returning a body with 204 is a protocol violation                     |
| `max(EMPLOYEES.keys()) + 1` | ID generation                            | Simple auto-increment strategy for in-memory storage                                                         |

<Tip>
  The `exclude_unset=True` argument to `model_dump()` is essential for `PUT` and `PATCH` endpoints. Without it, any optional field the client didn't send would be treated as `None` and could overwrite existing data.
</Tip>

***

## Endpoint Reference

| Method   | Path              | Description                                                | Success Code     |
| -------- | ----------------- | ---------------------------------------------------------- | ---------------- |
| `GET`    | `/employees`      | List all employees, with optional filtering and pagination | `200 OK`         |
| `GET`    | `/employees/{id}` | Get a single employee by ID                                | `200 OK`         |
| `POST`   | `/employees`      | Create a new employee                                      | `201 Created`    |
| `PUT`    | `/employees/{id}` | Update an employee's fields                                | `200 OK`         |
| `DELETE` | `/employees/{id}` | Remove an employee                                         | `204 No Content` |
