Building APIs with FastAPI

Reviewed & published by Brayan K

FastAPI is a modern, high-performance web framework that builds typed APIs from ordinary Python type hints. You get automatic validation, interactive OpenAPI docs, async support, and dependency injection — with very little boilerplate.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

🚀 1. Your First API

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, API!"}

▶️ 2. Running with uvicorn

FastAPI is an ASGI app; run it with the uvicorn server:

uvicorn main:app --reload

--reload restarts the server automatically when you edit code. Then open http://127.0.0.1:8000.

🛣️ 3. Path and Query Parameters

Names in the path become path parameters; extra typed arguments become query parameters:

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
def get_item(item_id: int, q: str | None = None):
    # /items/5?q=hello
    return {"item_id": item_id, "q": q}
ParameterComes from
item_idThe URL path (in curly braces)
qThe query string (?q=...)

Because item_id is typed int, FastAPI rejects /items/abc with a clear error.

📦 4. Request Bodies with Pydantic

Type a parameter as a Pydantic model and FastAPI parses and validates the JSON body:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float
    in_stock: bool = True

@app.post("/items")
def create_item(item: Item):
    return {"created": item.name, "price": item.price}

Send invalid JSON and FastAPI responds with a structured 422 error describing each problem.

🧪 Worked Example: A Complete Task API You Can Run

This pulls together everything from the sections above: a path parameter, a Pydantic body, a real HTTP status code, and a proper error. Read every comment before running it.

# main.py - a tiny task API, complete and runnable.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="Task API")

# A Pydantic model describes the SHAPE of the JSON you are willing to accept.
class Task(BaseModel):
    title: str
    done: bool = False        # has a default, so the client may leave it out

# A pretend database: task id -> task data.
tasks: dict[int, dict] = {1: {"title": "Write lesson", "done": True}}

@app.get("/tasks/{task_id}")      # {task_id} inside the path = a path parameter
def get_task(task_id: int):       # the int hint both converts AND validates it
    if task_id not in tasks:
        # Raising HTTPException is how you send back a real error status.
        raise HTTPException(status_code=404, detail="Task not found")
    return {"id": task_id, **tasks[task_id]}   # ** merges the stored dict in

@app.post("/tasks", status_code=201)   # 201 is the correct status for "created"
def create_task(task: Task):           # typed as the model -> FastAPI parses the JSON body
    new_id = max(tasks) + 1
    tasks[new_id] = task.model_dump()  # model_dump() turns the model back into a plain dict
    return {"id": new_id, **tasks[new_id]}

if __name__ == "__main__":
    # TestClient sends requests straight into the app - no port, no network.
    from fastapi.testclient import TestClient
    client = TestClient(app)

    r = client.get("/tasks/1")                              # the task that already exists
    print(r.status_code, r.json())

    r = client.post("/tasks", json={"title": "Buy milk"})   # 'done' left out -> default False
    print(r.status_code, r.json())

    r = client.get("/tasks/99")                             # nothing there -> your 404
    print(r.status_code, r.json())

    r = client.get("/tasks/abc")                            # 'abc' is not an int -> 422
    print(r.status_code, r.json()["detail"][0]["msg"])

# Expected output (a library deprecation warning may also appear above these lines,
# depending on your installed versions):
# 200 {'id': 1, 'title': 'Write lesson', 'done': True}
# 201 {'id': 2, 'title': 'Buy milk', 'done': False}
# 404 {'detail': 'Task not found'}
# 422 Input should be a valid integer, unable to parse string as an integer

# ✅ Expected output:
# 200 {'id': 1, 'title': 'Write lesson', 'done': True}
# 201 {'id': 2, 'title': 'Buy milk', 'done': False}
# 404 {'detail': 'Task not found'}
# 422 Input should be a valid integer, unable to parse string as an integer

Notice you never wrote a single line of validation. The int hint produced the 422, the model produced the default done: false, and your one raise produced the 404.

📖 5. Automatic Docs

With the server running, visit these URLs in your browser:

URLWhat you get
/docsInteractive Swagger UI
/redocClean ReDoc documentation
/openapi.jsonThe raw OpenAPI schema

You can try every endpoint right from /docs — no extra setup required.

⚡ 6. Async Endpoints

Use async def when an endpoint awaits I/O like a database or external API:

from fastapi import FastAPI

app = FastAPI()

@app.get("/data")
async def get_data():
    # await some_async_db_call()
    return {"status": "ok"}

FastAPI supports both def and async def handlers; use async when you need to await something.

🔌 7. Dependency Injection

Depends() lets you share logic (auth, db sessions, common params) across endpoints:

from fastapi import FastAPI, Depends

app = FastAPI()

def pagination(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

@app.get("/users")
def list_users(page: dict = Depends(pagination)):
    return {"page": page}

FastAPI calls pagination, validates its parameters, and passes the result in as page.

🎯 Your Turn: Finish the Endpoints

Three blanks stand between this file and a working API: one type hint, one HTTP method, and one model annotation. Fill them in, save the file, and run it with python — the TestClient at the bottom prints the four lines you need to match.

# 🎯 YOUR TURN — replace each ___ then run this file with:  python your_file.py
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel

app = FastAPI()

class Signup(BaseModel):
    email: str
    age: ___                  # 👉 age must be a whole number - which type is that?

@app.___("/hello/{name}")     # 👉 reading data uses which method? get or post?
def hello(name: str, excited: bool = False):
    # 'name' comes from the path; 'excited' has a default, so it is a query parameter.
    ending = "!" if excited else "."
    return {"greeting": f"Hello {name}{ending}"}

@app.post("/signup")
def signup(user: ___):        # 👉 annotate it with the model so the JSON body is validated
    return {"welcome": user.email, "age_next_year": user.age + 1}

client = TestClient(app)
print(client.get("/hello/Sam").json())
print(client.get("/hello/Sam?excited=true").json())
print(client.post("/signup", json={"email": "sam@example.com", "age": 29}).json())
print(client.post("/signup", json={"email": "sam@example.com", "age": "thirty"}).status_code)

# ✅ Expected output:
# {'greeting': 'Hello Sam.'}
# {'greeting': 'Hello Sam!'}
# {'welcome': 'sam@example.com', 'age_next_year': 30}
# 422

🎉 Conclusion

With FastAPI you can:

✔ Build typed routes with simple decorators

✔ Validate path, query, and body data automatically

✔ Get free interactive OpenAPI docs at /docs

✔ Write async endpoints for high concurrency

✔ Share logic cleanly with dependency injection

Type hints do double duty: they document and validate your API at once.

🎯 Mini-Challenge: A Temperature API

Nothing is written for you here. Build a one-endpoint API that converts Celsius to Fahrenheit and refuses physically impossible input, then prove it works with a TestClient exactly as the worked example did.

# 🎯 MINI-CHALLENGE: a temperature API
#
# 1. Import FastAPI and HTTPException from fastapi, and TestClient from
#    fastapi.testclient. Create the app.
# 2. Add GET /convert/{celsius} where celsius is typed as float.
#    - Normally return {"celsius": celsius, "fahrenheit": <the conversion>}
#      where the conversion is  celsius * 9 / 5 + 32  rounded to 1 decimal place.
#    - If celsius is below -273.15, raise HTTPException with status_code=400
#      and detail="Below absolute zero".
# 3. At the bottom create a TestClient and print:
#    - the .json() of GET /convert/100
#    - the .status_code and .json() of GET /convert/-300, on one line
#
# ✅ Expected output:
# {'celsius': 100.0, 'fahrenheit': 212.0}
# 400 {'detail': 'Below absolute zero'}

# your code here

📋 Quick Reference — FastAPI

SyntaxWhat it does
app = FastAPI()Create the application
@app.get('/items/{id}')Register a GET route
item: ItemModelValidated request body
Depends(fn)Dependency injection
uvicorn main:app --reloadRun the dev server

🎉 Great work! You've completed this lesson.

You can now build typed, documented, validated web APIs with FastAPI.

Practice quiz

How do you create a FastAPI application instance?

  • app = FastAPI()
  • app = fastapi.create()
  • app = App()
  • app = Server()

Answer: app = FastAPI(). You import FastAPI and instantiate it: app = FastAPI(); this app object holds all your routes.

Which decorator registers a handler for HTTP GET on a path?

  • @get('/')
  • @app.route('/')
  • @app.get('/')
  • @app.handle('/')

Answer: @app.get('/'). FastAPI uses one decorator per method, e.g. @app.get('/') and @app.post('/items').

In @app.get('/items/{item_id}'), how is item_id passed to the function?

  • It must be parsed manually
  • As a path parameter argument
  • As a global variable
  • Through request.body

Answer: As a path parameter argument. Names in {curly braces} become path parameters; declare a same-named function argument with a type hint.

If a function parameter has a type hint but is NOT in the path, FastAPI treats it as a what?

  • Path parameter
  • Header
  • Cookie
  • Query parameter

Answer: Query parameter. Parameters not present in the path are interpreted as query string parameters (e.g. ?limit=10).

How does FastAPI validate and parse a JSON request body?

  • With a Pydantic BaseModel type-hinted parameter
  • With a regular dict argument
  • With request.json() only
  • It does not validate bodies

Answer: With a Pydantic BaseModel type-hinted parameter. Declare a parameter typed as a Pydantic BaseModel; FastAPI parses, validates, and injects the body.

Where are the automatic interactive Swagger UI docs served by default?

  • /api/docs
  • /docs
  • /openapi
  • /swagger

Answer: /docs. FastAPI auto-generates interactive Swagger UI at /docs (and ReDoc at /redoc) from your type hints.

What keyword lets an endpoint function use await for non-blocking I/O?

  • yield
  • defer
  • parallel
  • async

Answer: async. Declaring 'async def' makes the path operation a coroutine so you can await async calls without blocking.

Which function provides dependency injection in FastAPI?

  • Provide()
  • Require()
  • Depends()
  • Inject()

Answer: Depends(). Depends() declares a dependency; FastAPI resolves it and passes the result into your endpoint.

Which ASGI server is commonly used to run a FastAPI app in development?

  • gunicorn-wsgi
  • uvicorn
  • flask run
  • node

Answer: uvicorn. FastAPI is an ASGI app; uvicorn is the standard ASGI server, e.g. 'uvicorn main:app --reload'.

What open standard does FastAPI generate to describe your API automatically?

  • OpenAPI
  • RAML
  • GraphQL schema
  • WSDL

Answer: OpenAPI. FastAPI generates an OpenAPI schema from your routes and types, which powers the /docs and /redoc UIs.

Continue this course