Home / Courses / FastAPI for Beginners / II — Hands-On Practice
II — Hands-On Practice
Article 3 of 6

Hands-On: Your First Endpoint & Basic CRUD

Setting up a FastAPI project from scratch, writing your first GET/POST endpoints with in-memory storage, and exploring the built-in interactive docs at `/docs`.

July 10, 2026

Now that we understand the core concepts and application anatomy, it's time to get hands-on. We'll build task-tracker-api — a simple REST API for tracking tasks — from scratch, starting with in-memory storage (data is lost on every restart). This is deliberately kept simple, because this article's focus is purely on the basic mechanics of FastAPI: defining endpoints, reading parameters, and running the server. Stricter validation and a real database follow in the next two articles.

The goal for this article: a FastAPI app running locally, with endpoints to view and create tasks, explorable through the built-in interactive documentation.

Project Setup

The steps:

  • Create a project folder and move into it:
  •    mkdir task-tracker-api
       cd task-tracker-api
       
  • Create and activate a virtual environment (standard Python practice so this project's dependencies don't clash with other projects):
  •    python3 -m venv .venv
       source .venv/bin/activate   # Windows: .venv\Scripts\activate
       
  • Install FastAPI and Uvicorn (the ASGI server that will run our app):
  •    pip install fastapi "uvicorn[standard]"
       

    uvicorn[standard] includes a few optional dependencies (like watchfiles for auto-reload) that we'll use often during development.

    First Endpoint: Health Check

    Create a main.py file:

    from fastapi import FastAPI
    
    app = FastAPI(title="Task Tracker API")
    
    
    @app.get("/health")
    def health_check():
        return {"status": "ok"}
    

    Run the server:

    uvicorn main:app --reload
    

    A few things worth understanding about this command:

  • main:app — means "look for an object named app in the file main.py". Adjust it if your file or object name differs.
  • --reload — the server automatically restarts every time we save a code change. Very handy during development, but never use it in production.
  • Open http://localhost:8000/health in your browser — you'll see {"status":"ok"}. Then open http://localhost:8000/docs — this is the interactive documentation (Swagger UI) covered in the previous article, already showing up automatically without us writing any documentation at all.

    Setting Up In-Memory "Storage"

    For now, we'll store tasks in a plain Python list. Add this to main.py, right below the app = FastAPI(...) line:

    tasks = [
        {"id": 1, "title": "Learn FastAPI concepts", "done": False},
        {"id": 2, "title": "Build the first endpoint", "done": False},
    ]
    next_id = 3
    

    This is purely for learning purposes — this data will be lost every time the server restarts. In Part 5, we'll replace this with a real database.

    GET Endpoints — Path Parameters and Query Parameters

    Add the following three endpoints:

    from fastapi import HTTPException
    
    
    @app.get("/api/tasks")
    def list_tasks(done: bool | None = None):
        if done is None:
            return tasks
        return [t for t in tasks if t["done"] == done]
    
    
    @app.get("/api/tasks/{task_id}")
    def get_task(task_id: int):
        for task in tasks:
            if task["id"] == task_id:
                return task
        raise HTTPException(status_code=404, detail="Task not found")
    

    Notice two things we already covered conceptually in the previous article — now we get to see them in practice:

  • list_tasks(done: bool | None = None)done isn't part of the path, so it automatically becomes an optional query parameter. Try hitting /api/tasks?done=true and /api/tasks?done=false in the browser, and compare the results with plain /api/tasks.
  • get_task(task_id: int)task_id appears in the path ({task_id}), so it's a path parameter, required to be a number. Try hitting /api/tasks/abc — FastAPI automatically rejects it with a validation error, because abc isn't an int.
  • raise HTTPException(status_code=404, ...) — the explicit way to stop execution and return an error with a specific status code. We'll go deeper on this in the next article.
  • POST Endpoint — Accepting a Request Body

    Now for the endpoint that creates a new task. This needs a Pydantic model to define the expected shape of the data:

    from pydantic import BaseModel
    
    
    class TaskCreate(BaseModel):
        title: str
    
    
    @app.post("/api/tasks", status_code=201)
    def create_task(payload: TaskCreate):
        global next_id
        task = {"id": next_id, "title": payload.title, "done": False}
        tasks.append(task)
        next_id += 1
        return task
    

    Save the file and let --reload handle the automatic restart. Now open /docs, find the POST /api/tasks section, click "Try it out", fill in title, then click Execute. Notice three things:

  • Swagger UI already knows the body must look like {"title": "..."}, because it reads the schema straight from TaskCreate.
  • The response shows up right on the same page — we don't need Postman or a separate tool to try this API.
  • Try leaving title empty, or removing the field entirely — FastAPI automatically rejects it with a 422 status, before our create_task code ever runs.
  • Manual Testing With curl

    Besides /docs, we can also test from the terminal:

    curl http://localhost:8000/api/tasks
    
    curl -X POST http://localhost:8000/api/tasks \
      -H "Content-Type: application/json" \
      -d '{"title": "Deploy to production"}'
    
    curl http://localhost:8000/api/tasks/999
    

    That last command returns {"detail":"Task not found"} with a 404 status — exactly what we defined in get_task.

    Where main.py Stands Now

    After all the steps above, our main.py contains: the app setup, in-memory data, three endpoints (GET /health, GET /api/tasks, GET /api/tasks/{id}), and one POST /api/tasks endpoint. Over the next two articles this code will grow: validation gets stricter, and then it gets split into several separate files once we connect it to a real database.

    Summary

    We just built our first API with FastAPI: GET and POST endpoints, path parameters, query parameters, a request body via a Pydantic model, and exploring it all through the automatic interactive documentation. We didn't write a single line of documentation by hand — all of it was generated from the exact same code we ran.

    Topics

    FastAPIPython