Installing FastAPI and your first app¶
This chapter gets a FastAPI app running and builds the same small API two ways — once in FastAPI, once in Django — so the shape of the difference is concrete from the start. Both versions ship as companion projects you can run and test.
Installing¶
FastAPI is a normal PyPI package. Create a virtual environment and install
it — the [standard] extra pulls in the pieces you'll actually want in
development (the Uvicorn server, the fastapi CLI, httpx for the test
client, Jinja2, and form/file support):
That's the whole install. There is no startproject, no settings.py, no
generated directory tree — you create an empty .py file and start
writing.
From Django:
pip install Djangothendjango-admin startproject config .scaffolds a project package (settings.py,urls.py,wsgi.py,asgi.py) and amanage.py. FastAPI scaffolds nothing. The upside is zero ceremony; the downside is that structure, settings, and entry points are decisions you make yourself (topic 1 has a whole chapter on project layout).
Your first app¶
Put this in main.py:
from fastapi import FastAPI, HTTPException
app = FastAPI(title="First App (FastAPI)")
ITEMS = [
{"id": 1, "name": "Widget"},
{"id": 2, "name": "Gadget"},
{"id": 3, "name": "Gizmo"},
]
@app.get("/")
def read_root():
return {"message": "It works!"}
@app.get("/items")
def list_items(limit: int = 10):
return {"items": ITEMS[:limit]}
@app.get("/items/{item_id}")
def get_item(item_id: int):
for item in ITEMS:
if item["id"] == item_id:
return item
raise HTTPException(status_code=404, detail="Item not found")
Three things are happening, all driven by the function signatures:
- Routing is a decorator.
@app.get("/items")binds a path and HTTP method directly to the function. There is no separate URL table. - Return a
dict, get JSON. FastAPI serializes the return value and setscontent-type: application/json. You don't touch a response object for the common case. - Parameters are typed and validated.
item_id: intmeans the path segment is parsed and validated as an integer;limit: int = 10becomes an optional query parameter with a default.
Run it¶
fastapi dev main.py # the fastapi[standard] CLI, with auto-reload
# or, equivalently:
uvicorn main:app --reload
Open http://127.0.0.1:8000/items, then http://127.0.0.1:8000/docs for the interactive Swagger UI that FastAPI generated from those same signatures.
From Django:
fastapi dev main.pyis themanage.py runserverof this world — a dev server with auto-reload. The difference underneath:runserveris a WSGI dev server, while Uvicorn is an ASGI server, and the/docsUI has norunserverequivalent — you'd reach for DRF's Browsable API ordrf-spectacularto get something comparable.
The same API in Django¶
Here is the identical API built with Django views. Django needs a project
package, an app, a URL table, and views that return JsonResponse:
# config/urls.py
from django.urls import path
from items import views
urlpatterns = [
path("", views.read_root),
path("items", views.list_items),
path("items/<int:item_id>", views.get_item),
]
# items/views.py
from django.http import JsonResponse
ITEMS = [
{"id": 1, "name": "Widget"},
{"id": 2, "name": "Gadget"},
{"id": 3, "name": "Gizmo"},
]
def read_root(request):
return JsonResponse({"message": "It works!"})
def list_items(request):
limit = int(request.GET.get("limit", 10)) # convert/validate by hand
return JsonResponse({"items": ITEMS[:limit]})
def get_item(request, item_id):
for item in ITEMS:
if item["id"] == item_id:
return JsonResponse(item)
return JsonResponse({"detail": "Item not found"}, status=404)
Line them up and the division of labour is clear:
| FastAPI | Django | |
|---|---|---|
| Bind URL → handler | @app.get("/items") decorator |
entry in urlpatterns |
| Read a query param | limit: int = 10 (parsed + validated) |
int(request.GET.get("limit", 10)) (manual) |
| Return JSON | return {...} |
return JsonResponse({...}) |
| Signal "not found" | raise HTTPException(404, ...) |
JsonResponse({...}, status=404) |
| Interactive docs | built in at /docs |
not built in |
One difference worth noticing now: bad input¶
Request /items/abc — a non-integer where an integer id is expected — and
the two frameworks diverge:
GET /items/abc
# FastAPI → 422 Unprocessable Entity
# {"detail":[{"type":"int_parsing","loc":["path","item_id"], ...}]}
# Django → 404 Not Found
FastAPI declares item_id: int, so it validates the path and returns a
structured 422 describing what was wrong. Django's <int:item_id> URL
converter simply doesn't match a non-integer, so the resolver falls
through to a 404 — the view never runs. Same intent (reject bad input),
different mechanism and different status code. Both companion projects have
a test pinning this exact behaviour.
From Django: In DRF you'd get FastAPI-style
400/422validation errors from a serializer, but plain Django leaves per-parameter validation to you — theint()call inlist_itemsabove would raiseValueError(a500) on?limit=abc. FastAPI's "the type hint is the validation" model is closer to DRF than to Django views, but it's built into the function signature rather than a separate serializer class.
Companion projects¶
Both apps above are complete, runnable projects under this chapter's
02-demo/ folder:
02-demo/fastapi/(download) —main.py, tests,requirements.txt. Run withfastapi dev main.py; test withpytest.02-demo/django/(download) —config/project +items/app. Run withpython manage.py runserver; test withpython manage.py test.
Each side has a small test suite (six tests) asserting the responses shown
here, including the 422/404 difference. Every chapter from here on
ships a pair like this.