Creates an endpoint at / that responds to http GET requests with the json {"message": "Hello World"}.
Running a FastAPI app
There are a couple of options for running your app,
Running from Python using uvicorn (an HTTP server based on uv):
uvicorn.run(app, host="0.0.0.0", port=8181)
INFO: Started server process [84680]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8181 (Press CTRL+C to quit)
Running from the command line using uvicorn
uvicorn ex1.app:app --host 0.0.0.0 --port 8080
INFO: Started server process [99168]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
Running from the shell using fastapi (you may need to run uv add 'fastapi[standard]' for this to work)
fastapi run ex1/app.py
FastAPI Starting production server 🚀
Searching for package file structure from directories with __init__.py files
Importing from /Users/rundel/Desktop/Sta663-Sp26/website/static/slides/Lec17/ex1
module 🐍 app.py
code Importing the FastAPI app object from the module with the following code:
from app import app
app Using import string: app:app
server Server started at http://0.0.0.0:8000
server Documentation at http://0.0.0.0:8000/docs
Logs:
INFO Started server process [99526]
INFO Waiting for application startup.
INFO Application startup complete.
INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Both run and dev commands are available. The primary difference is the latter has auto-reload enabled which restarts the server when the underlying code changes. dev also binds 127.0.0.1 by default while run binds 0.0.0.0.
from fastapi import FastAPIapp = FastAPI()@app.get("/")asyncdef root():return {"message": "Hello World"}@app.get("/add")asyncdef add(x: int, y: int=0):return {"result": x+y}@app.get("/user/{user_id}")asyncdef user_id(user_id: int, name: str|None=None): res = {"user_id": user_id}if name isnotNone: res["name"] = namereturn res
async?
You may have noticed that all of the function definitions make use of async def f(...) - this allows FastAPI to execute these functions asynchronously.
If you don’t know what this is generally you don’t need to worry about it, but some general advice is:
If you are using a third-party library that tells you to make calls using the await keyword then you definitely need async.
If your function, or a library your function uses, communicates with something (e.g. a database, an API, the file system, etc.) and does not support await, then don’t use async.
If neither of the above apply, generally you should default to using async (as long as you avoid blocking calls inside the function).
Query parameters
Like plumber, endpoint functions’ arguments are interpreted as query parameters. All arguments without defaults are assumed to be required.
If type hinting is used when defining your function then FastAPI will attempt to validate the user’s inputs based on those types.
r = requests.get(url+"/add?x=1.0&y=2.0")r.status_code
200
r.json()
{'result': 3}
r = requests.get(url+"/add?x=abc&y=1")r.status_code
422
r.json()
{'detail':
[{
'type': 'int_parsing',
'loc': ['query', 'x'],
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'input': 'abc'
}]
}
r = requests.get(url+"/add?x=1.5&y=2.")r.status_code
422
r.json()
{'detail':
[{
'type': 'int_parsing',
'loc': ['query', 'x'],
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'input': '1.5'
}, {
'type': 'int_parsing',
'loc': ['query', 'y'],
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'input': '2.'
}]
}
Path parameters
Again, like plumber, arguments can be passed to the API using the request path - these are indicated using {} in the path definition and then having a matching argument name in the function definition.
r = requests.get(url+"/user/1234?name=Colin")r.status_code
200
r.json()
{'user_id': 1234, 'name': 'Colin'}
r = requests.get(url+"/user/3141")r.status_code
200
r.json()
{'user_id': 3141}
r = requests.get(url+"/user/Colin")r.status_code
422
r.json()
{'detail':
[{
'type': 'int_parsing',
'loc': ['path', 'user_id'],
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'input': 'Colin'
}]
}
HTTP Methods
The HTTP method used in the decorator determines the type of operation the endpoint performs. REST conventions suggest:
Method
Purpose
Decorator
GET
Retrieve data
@app.get()
POST
Create a new resource
@app.post()
PUT
Replace a resource entirely
@app.put()
PATCH
Partially update a resource
@app.patch()
DELETE
Remove a resource
@app.delete()
Query and path parameters are most natural with GET and DELETE.
POST, PUT, and PATCH typically receive data via a request body.
Request body
When making PUT, POST, or PATCH requests, we are usually sending data to the API via the body of our request.
FastAPI makes use of pydantic models to define the expected body content. I would like to avoid getting into the weeds of pydantic and typing as much as possible, so we will go with the most basic use case.
The following pydantic model specifies an expected body that contains name and price entries that are a string and float respectively, and optionally a description string and tax float.
With just that Python type declaration, FastAPI will:
Read the body of the request as JSON.
Convert the corresponding types (if needed).
Validate the data.
If the data is invalid, it will return a nice and clear error, indicating exactly where and what was the incorrect data.
Give you the received data in the parameter item.
As you declared it in the function to be of type Item, you will also have all the editor support (completion, etc) for all of the attributes and their types.
Generate JSON Schema definitions for your model, you can also use them anywhere else you like if it makes sense for your project.
Those schemas will be part of the generated OpenAPI schema, and used by the automatic documentation UIs.
Body vs path & query parameters
Endpoints can use any mixture of body, path, and query parameters.
FastAPI uses the following rules to determine what each argument is:
The function parameters will be recognized as follows:
If the parameter is also declared in the path, it will be used as a path parameter.
If the parameter is of a singular type (like int, float, str, bool, etc) it will be interpreted as a query parameter.
If the parameter is declared to be of the type of a Pydantic model, it will be interpreted as a request body.
Error handling - HTTPException
For intentional errors (e.g. a requested resource not found), FastAPI provides HTTPException:
from fastapi import HTTPException@app.get("/items/{item_id}")asyncdef get_item(item_id: int):if item_id >=len(items):raise HTTPException(status_code=404, detail="Item not found")return items[item_id]
The media_type tells the client how to interpret the bytes — a browser or Python client receiving image/png knows to render it as an image rather than text.