Difficulty: Beginner
Building your first FastAPI application is straightforward once you understand the core concepts: the application instance, path operation decorators, and handler functions. Let us walk through building a simple but complete API step by step.
The foundation of every FastAPI app is the application instance, created with app = FastAPI(). This object is the main entry point that Uvicorn uses to serve your API. You can pass configuration options to it like title, description, version, and more. The app instance has methods corresponding to HTTP methods: app.get(), app.post(), app.put(), app.delete(), app.patch(), etc. These are used as decorators on your handler functions.
Path operation decorators like @app.get("/") define two things: the HTTP method (GET, POST, etc.) and the URL path. The decorated function is called a 'path operation function' or 'route handler.' When a request matches the HTTP method and path, FastAPI calls this function and returns its result as a JSON response. The function can be either synchronous (def) or asynchronous (async def) - FastAPI handles both correctly.
FastAPI automatically converts the return value of your handler function to JSON. You can return Python dictionaries, lists, Pydantic models, or any JSON-serializable object. If you return a dictionary like {"name": "Alice", "age": 30}, FastAPI serializes it to a JSON response with the appropriate Content-Type header. For more control over the response, you can use FastAPI's Response classes directly.
The request-response cycle in FastAPI follows a clear path: a client sends an HTTP request, Uvicorn receives it and passes it to FastAPI, FastAPI matches the request to a route based on the method and path, the route handler executes and returns a value, FastAPI serializes the return value to JSON, and the response is sent back to the client. If any step fails (like validation), FastAPI returns an appropriate error response.
FastAPI supports all standard HTTP methods. GET is used for retrieving data, POST for creating new resources, PUT for replacing existing resources entirely, PATCH for partial updates, and DELETE for removing resources. Each method has a corresponding decorator: @app.get(), @app.post(), @app.put(), @app.patch(), @app.delete(). You can also use @app.api_route() to handle multiple methods with a single function, though this is less common.
A practical first application might be a simple in-memory todo list API. This demonstrates CRUD (Create, Read, Update, Delete) operations without needing a database. You store items in a Python list or dictionary, define endpoints for each operation, and use Pydantic models for request validation.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class TodoCreate(BaseModel):
title: str
completed: bool = False
class TodoResponse(BaseModel):
id: int
title: str
completed: bool
todos: dict[int, dict] = {}
next_id = 1
@app.get("/todos", response_model=list[TodoResponse])
def list_todos():
return list(todos.values())
@app.post("/todos", response_model=TodoResponse, status_code=201)
def create_todo(todo: TodoCreate):
global next_id
new_todo = {"id": next_id, "title": todo.title, "completed": todo.completed}
todos[next_id] = new_todo
next_id += 1
return new_todo
@app.get("/todos/{todo_id}", response_model=TodoResponse)
def get_todo(todo_id: int):
if todo_id not in todos:
raise HTTPException(status_code=404, detail="Todo not found")
return todos[todo_id]
@app.delete("/todos/{todo_id}")
def delete_todo(todo_id: int):
if todo_id not in todos:
raise HTTPException(status_code=404, detail="Todo not found")
del todos[todo_id]
return {"message": "Todo deleted"}
This demonstrates a full CRUD API with in-memory storage. POST creates items, GET retrieves them, and DELETE removes them. HTTPException is used to return 404 errors when items are not found. The response_model parameter ensures consistent response structure.
from fastapi import FastAPI
import asyncio
app = FastAPI()
# Synchronous handler - runs in a thread pool
@app.get("/sync")
def sync_endpoint():
# Blocking operations are fine here
# FastAPI runs this in a thread pool automatically
return {"type": "synchronous"}
# Asynchronous handler - runs on the event loop
@app.get("/async")
async def async_endpoint():
# Use await for async operations
await asyncio.sleep(0.1) # Simulating async I/O
return {"type": "asynchronous"}
# Rule of thumb:
# Use 'async def' when calling async libraries (httpx, asyncpg, aiofiles)
# Use 'def' when calling sync libraries (requests, psycopg2, open())
FastAPI handles both sync and async handlers. Sync handlers (def) are run in a thread pool so they do not block the event loop. Async handlers (async def) run directly on the event loop. Use async when working with async libraries for better performance.
from fastapi import FastAPI
from fastapi.responses import (
JSONResponse,
PlainTextResponse,
HTMLResponse,
RedirectResponse,
)
app = FastAPI()
@app.get("/json")
def json_response():
# Default: returns JSON
return {"format": "json"}
@app.get("/text", response_class=PlainTextResponse)
def text_response():
return "This is plain text"
@app.get("/html", response_class=HTMLResponse)
def html_response():
return "<h1>Hello from HTML</h1>"
@app.get("/redirect")
def redirect_response():
return RedirectResponse(url="/json")
@app.get("/custom")
def custom_response():
return JSONResponse(
content={"custom": True},
status_code=201,
headers={"X-Custom-Header": "my-value"},
)
While JSON is the default, FastAPI supports many response types. Use response_class to change the default, or return Response objects directly for full control over status codes, headers, and content type.
Decorators, Route Handlers, JSON Responses, Path Operations, Request-Response Cycle