Clients can pass their request data to FastAPI endpoint URLs through path parameters, queryparameters, or headers to pursue service transactions. There arestandards and ways to use these parameters to obtain incoming requests. Depending on the goal of the services, we use these parameters to influence and build the necessary responses the clients need. But before we discuss these various parameter types, let us explore first how we usetype hinting in FastAPI’s local parameter declaration.
Parameter type declaration
All requestparameters are required to be type-declared in the method signature of the service method applying thePEP 484 standard calledtype hints. FastAPI supports common typessuch asNone
,bool
,int
, andfloat
and container types such aslist
,tuple
,dict
,set
,frozenset
, anddeque
. Other complex Python types such asdatetime.date
,datetime.time
,datetime.datetime
,datetime.delta
,UUID
,bytes
, andDecimal
are also supported.
The framework also supports the data types included in Python’styping
module, responsible fortype hints. These data types are standard notations for Python and variable typeannotations that can help to pursue type checking and model validation during compilation, such asOptional
,List
,Dict
,Set
,Union
,Tuple
,FrozenSet
,Iterable
, andDeque
.
Path parameters
FastAPI allows youto obtain request data from the endpoint URL of an API througha path parameter or path variable that makes the URL somewhat dynamic. This parameter holds a value that becomes part of a URL indicated by curly braces ({}
). After setting off these path parameters within the URL, FastAPI requires these parameters to be declared by applyingtype hints.
The followingdelete_user()
service is aDELETE
API method that uses ausername
path parameter to search for a login record for deletion:
@app.delete("/ch01/login/remove/{username}")def delete_user(username: str): if username == None: return {"message": "invalid user"}else: del valid_users[username] return {"message": "deleted user"}
Multiple path parameters are acceptable if the leftmost variables are more likely to be filled with values than the rightmost variables. In other words, the importance of the leftmost path variables will make the process more relevant and correct than those on the right. This standard is applied to ensure that the endpoint URL will not look like other URLs, which might cause some conflicts and confusion. The followinglogin_with_token()
service follows this standard, sinceusername
is a primary key and is as strong as, or even stronger than, its next parameter,password
. There is an assurance that the URL will always look unique every time the endpoint is accessed becauseusername
will always be required, as well aspassword
:
@app.get("/ch01/login/{username}/{password}")def login_with_token(username: str,password:str, id: UUID): if valid_users.get(username) == None: return {"message": "user does not exist"} else: user = valid_users[username] if user.id == id and checkpw(password.encode(), user.passphrase): return user else: return {"message": "invalid user"}
Unlike other web frameworks, FastAPI is not friendly with endpoint URLs that belong to base paths ortop-level domain paths with different subdirectories. Thisoccurrence happens when we have dynamic URL patterns that look the same as the other fixed endpoint URLs when assigned a specific path variable. These fixed URLs are implemented sequentially after these dynamic URLs. An example of these are the following services:
@app.get("/ch01/login/{username}/{password}")def login_with_token(username: str,password:str, id: UUID): if valid_users.get(username) == None: return {"message": "user does not exist"} else: user = valid_users[username] if user.id == id and checkpw(password.encode(), user.passphrase.encode()): return user else: return {"message": "invalid user"}@app.get("/ch01/login/details/info")def login_info(): return {"message": "username and password are needed"}
This will give us anHTTP Status Code 422 (Unprocessable Entity) when accessinghttp://localhost:8080/ch01/login/details/info
. There should be no problem accessing the URL, since the API service is almost a stub or trivial JSON data. What happenedin this scenario is that the fixed path’sdetails
andinfo
pathdirectories were treated asusername
andpassword
parameter values, respectively. Because of confusion, the built-in data validation of FastAPI will show us a JSON-formatted error message that says,{"detail":[{"loc":["query","id"],"msg":"field required","type":"value_error.missing"}]}
. To fix this problem, all fixed paths should be declared first before the dynamic endpoint URLs with path parameters. Thus, the precedinglogin_info()
service should be declared first beforelogin_with_token()
.
Query parameters
A query parameter is akey–value pair supplied after the end of an endpoint URL, indicated by aquestion mark (?
). Just like the path parameter, this also holds therequest data. An API service can manage a series of query parameters separated by an ampersand (&
). Like in path parameters, all query parameters are also declared in the service method. The followinglogin service is a perfect specimen that uses query parameters:
@app.get("/ch01/login/")def login(username: str,password: str): if valid_users.get(username) == None: return {"message": "user does not exist"} else: user = valid_users.get(username) if checkpw(password.encode(), user.passphrase.encode()): return user else: return {"message": "invalid user"}
Thelogin
service method usesusername
andpassword
as query parameters in thestr
types. Both are required parameters, and assigning them withNone
as parameter values will give a compiler error.
FastAPI supportsquery parameters that are complex types, such aslist
anddict
. Butthese Python collection types cannot specify the type of objects to store unless we apply thegeneric type hints for Python collections. The followingdelete_users()
andupdate_profile_names()
APIs use generic type hints,List
andDict
, in declaring query parameters that are container types with type checking and data validation:
from typing import Optional,List,Dict@app.delete("/ch01/login/remove/all")def delete_users(usernames: List[str]): for user in usernames: del valid_users[user] return {"message": "deleted users"}@app.patch("/ch01/account/profile/update/names/{username}")def update_profile_names(username: str, id: UUID, new_names: Dict[str, str]): if valid_users.get(username) == None: return {"message": "user does not exist"} elif new_names == None: return {"message": "new names are required"} else: user = valid_users.get(username) if user.id == id: profile = valid_profiles[username] profile.firstname = new_names['fname'] profile.lastname = new_names['lname'] profile.middle_initial = new_names['mi'] valid_profiles[username] = profile return {"message": "successfully updated"} else: return {"message": "user does not exist"}
FastAPI alsoallows you to explicitly assign default values to servicefunction parameters.
Default parameters
There are times that weneed to specify default values to the query parameter(s) andpath parameter(s) of some API services to avoid validation error messages such asfield required
andvalue_error.missing
. Setting default values to parameters will allow the execution of an API method with or without supplying the parameter values. Depending on the requirement, assigned default values are usually0
for numeric types,False
for bool types, empty string for string types, an empty list ([]
) for List types, and an empty dictionary ({}
) forDict
types. The followingdelete pending users()
andchange_password()
services show us how to apply default values to the query parameter(s) and path parameter(s):
@app.delete("/ch01/delete/users/pending")def delete_pending_users(accounts: List[str] = []): for user in accounts: del pending_users[user] return {"message": "deleted pending users"}@app.get("/ch01/login/password/change")def change_password(username: str,old_passw: str = '', new_passw: str = ''): passwd_len = 8 if valid_users.get(username) == None: return {"message": "user does not exist"} elif old_passw == '' or new_passw == '': characters = ascii_lowercase temporary_passwd = ''.join(random.choice(characters) for i in range(passwd_len)) user = valid_users.get(username) user.password = temporary_passwd user.passphrase = hashpw(temporary_passwd.encode(),gensalt()) return user else: user = valid_users.get(username) if user.password == old_passw: user.password = new_passw user.passphrase = hashpw(new_pass.encode(),gensalt()) return user else: return {"message": "invalid user"}
delete_pending_users()
can be executed even without passing any accounts argument, since accounts will bealways an emptyList
by default. Likewise,change_password()
canstill continue its process without passing anyold_passwd
andnew_passw
, since they are both always defaulted to emptystr
.hashpw()
is abcrypt
utility function that generates a hashed passphrase from an autogeneratedsalt.
Optional parameters
If thepath and/orquery parameter(s) of a service is/are not necessarily needed to be supplied by theuser, meaning the API transactions can proceed with or withouttheir inclusion in the request transaction, then we set them asoptional. To declare an optional parameter, we need to import theOptional
type from thetyping
module and then use it to set the parameter. It should wrap the supposed data type of the parameter using brackets ([]
) and can haveany default value if needed. Assigning theOptional
parameter to aNone
value indicates that its exclusion from the parameter passing is allowed by the service, but it will hold aNone
value. The following services depict the use of optional parameters:
from typing importOptional, List, Dict@app.post("/ch01/login/username/unlock")def unlock_username(id:Optional[UUID] = None): if id == None: return {"message": "token needed"} else: for key, val in valid_users.items(): if val.id == id: return {"username": val.username} return {"message": "user does not exist"}@app.post("/ch01/login/password/unlock")def unlock_password(username: Optional[str] = None, id: Optional[UUID] = None): if username == None: return {"message": "username is required"} elif valid_users.get(username) == None: return {"message": "user does not exist"} else: if id == None: return {"message": "token needed"} else: user = valid_users.get(username) if user.id == id: return {"password": user.password} else: return {"message": "invalid token"}
In theonline academic discussion forum application, we have services such as the precedingunlock_username()
andunlock_password()
services that declare all their parameters asoptional
. Just do notforget to apply exception handling or defensivevalidation in your implementation when dealing with these kinds of parameters to avoidHTTP Status 500 (Internal Server Error).
Important note
The FastAPI framework does not allow you to directly assign theNone
value to a parameter just to declare anoptional parameter. Although this is allowed with the old Python behavior, this is no longer recommended in the current Python versions for the purpose of built-in type checking and model validation.
Mixing all types of parameters
If you are planning toimplement an API service method that declares optional, required, and default query and path parameters altogether, you can pursue it because the framework supports it, but approach it with some caution due to some standards and rules:
@app.patch("/ch01/account/profile/update/names/{username}")def update_profile_names(id: UUID,username: str = '' , new_names: Optional[Dict[str, str]] = None): if valid_users.get(username) == None: return {"message": "user does not exist"} elif new_names == None: return {"message": "new names are required"} else: user = valid_users.get(username) if user.id == id: profile = valid_profiles[username] profile.firstname = new_names['fname'] profile.lastname = new_names['lname'] profile.middle_initial = new_names['mi'] valid_profiles[username] = profile return {"message": "successfully updated"} else: return {"message": "user does not exist"}
The updated version of the precedingupdate_profile_names()
service declares ausername
path parameter, aUUID
id query parameter, and an optionalDict[str, str]
type. With mixed parameter types, all required parameters should be declaredfirst, followed by default parameters, and last in the parameter list should be the optional types. Disregarding this ordering rule will generate acompiler error.
Request body
Arequest body is a bodyof data in bytes transmitted from a client to a serverthrough aPOST
,PUT
,DELETE
, orPATCH
HTTP method operation. In FastAPI, a service must declare a model object to represent and capture this request body to be processed for further results.
To implement a model class for therequest body, you should first import theBaseModel
class from thepydantic
module. Then, create a subclass of it to utilize all the properties and behavior needed by the path operation in capturing the request body. Here are some of the data models used by our application:
from pydantic importBaseModelclass User(BaseModel): username: str password: strclass UserProfile(BaseModel): firstname: str lastname: str middle_initial: str age: Optional[int] = 0 salary: Optional[int] = 0 birthday: date user_type: UserType
The attributes of the model classes must be explicitly declared by applyingtype hints and utilizing the common and complex data types used in the parameter declaration. These attributes can also be set as required, default, and optional, just like in the parameters.
Moreover, thepydantic
moduleallows the creation of nested models, even the deeplynested ones. A sample of these is shown here:
class ForumPost(BaseModel): id: UUID topic: Optional[str] = None message: str post_type: PostType date_posted: datetime username: strclass ForumDiscussion(BaseModel): id: UUID main_post: ForumPost replies: Optional[List[ForumPost]] = None author: UserProfile
As seen in the preceding code, we have aForumPost
model, which has aPostType
model attribute, andForumDiscussion
, which has aList
attribute ofForumPost
, aForumPost
model attribute, and aUserProfile
attribute. This kind of modelblueprint is called anested model approach.
After creating these model classes, you can nowinject these objects into the services that are intendedto capture therequest body from the clients. The followingservices utilize ourUser
andUserProfile
model classes to manage the request body:
@app.post("/ch01/login/validate", response_model=ValidUser)def approve_user(user: User): if not valid_users.get(user.username) == None: returnValidUser(id=None, username = None, password = None, passphrase = None) else: valid_user =ValidUser(id=uuid1(), username= user.username, password = user.password, passphrase = hashpw(user.password.encode(), gensalt())) valid_users[user.username] = valid_user del pending_users[user.username] return valid_user@app.put("/ch01/account/profile/update/{username}")def update_profile(username: str,id: UUID, new_profile: UserProfile): if valid_users.get(username) == None: return {"message": "user does not exist"} else: user = valid_users.get(username) if user.id == id: valid_profiles[username] = new_profile return {"message": "successfully updated"} else: return {"message": "user does not exist"}
Models can be declaredrequired, with adefault instance value, oroptional in the service method, dependingon the specification of the API. Missing or incorrect detailssuch asinvalid password
orNone
values in theapprove_user()
service will emit theStatus Code 500 (Internal Server Error). How FastAPI handles exceptions will be part ofChapter 2,Exploring the Core Features, discussions.
Important note
There are two essential points we need to emphasize when dealing withBaseModel
class types. First, thepydantic
module has a built-in JSON encoder that converts the JSON-formatted request body to theBaseModel
object. So, there is no need create a custom converter to map the request body to theBaseModel
model. Second, to instantiate aBaseModel
class, all its required attributes must be initialized immediately through the constructor’s named parameters.
Request headers
In a request-response transaction, itis not only the parameters that areaccessible by the REST API methods but also the information that describes the context of the client where the request originated. Some common request headers such asUser-Agent
,Host
,Accept
,Accept-Language
,Accept-Encoding
,Referer
, andConnection
usually appear with request parameters and values during request transactions.
To access a request header, import first theHeader
function from thefastapi
module. Then, declare the variable that has the same name as the header in the method service asstr
types and initialize the variable by calling theHeader(None)
function. TheNone
argument enables theHeader()
function to declare the variable optionally, which is a best practice. For hyphenated request header names, the hyphen (-
) should be converted to an underscore (_
); otherwise, the Python compiler will flag a syntax error message. It is the task of theHeader()
function to convert the underscore (_
) to a hyphen (-
) during request header processing.
Our online academic discussion forum application has averify_headers()
servicethat retrieves core request headers needed to verify a client’s access to the application:
from fastapi importHeader@app.get("/ch01/headers/verify")def verify_headers(host: Optional[str] = Header(None), accept: Optional[str] = Header(None), accept_language: Optional[str] = Header(None), accept_encoding: Optional[str] = Header(None), user_agent: Optional[str] = Header(None)): request_headers["Host"] = host request_headers["Accept"] = accept request_headers["Accept-Language"] = accept_language request_headers["Accept-Encoding"] = accept_encoding request_headers["User-Agent"] = user_agent return request_headers
Important note
Non-inclusion of theHeader()
function call in the declaration will let FastAPI treat the variables asquery parameters. Be cautious also with the spelling of the local parameter names, since theyare the request header names per se except for the underscore.
Response data
All API services in FastAPIshould returnJSON
data, or it will be invalid and mayreturnNone
by default. These responses can be formed usingdict
,BaseModel
, orJSONResponse
objects. Discussions onJSONResponse
will be discussed in the succeeding chapters.
Thepydantic
module’s built-in JSON converter will manage the conversion of these custom responses to a JSON object, so there is no need to create a custom JSON encoder:
@app.post("/ch01/discussion/posts/add/{username}")def post_discussion(username: str, post: Post, post_type: PostType): if valid_users.get(username) == None: return{"message": "user does not exist"} elif not (discussion_posts.get(id) == None): return{"message": "post already exists"} else: forum_post = ForumPost(id=uuid1(), topic=post.topic, message=post.message, post_type=post_type, date_posted=post.date_posted, username=username) user = valid_profiles[username] forum = ForumDiscussion(id=uuid1(), main_post=forum_post, author=user, replies=list()) discussion_posts[forum.id] = forum returnforum
The precedingpost_discussion()
service returns two different hardcodeddict
objects, withmessage
as the key and an instantiatedForumDiscussion
model.
On the other hand, this framework allows us to specify the return type of a service method. The settingof the return type happens in theresponse_model
attributeof any of the@app
path operations. Unfortunately, the parameter only recognizesBaseModel
class types:
@app.post("/ch01/login/validate",response_model=ValidUser)def approve_user(user: User): if not valid_users.get(user.username) == None: returnValidUser(id=None, username = None, password = None, passphrase = None) else: valid_user = ValidUser(id=uuid1(), username= user.username, password = user.password, passphrase = hashpw(user.password.encode(), gensalt())) valid_users[user.username] = valid_user del pending_users[user.username] returnvalid_user
The precedingapprove_user()
service specifies the required return of the API method, which isValidUser
.
Now, let us explore how FastAPI handles form parameters.