Designing and implementing REST APIs
The Representation State Transfer (REST) API makes up the rules, processes, and tools that allow interaction among microservices. These are method services that are identified and executed through their endpoint URLs. Nowadays, focusing on API methods before building a whole application is one of the most popular and effective microservices design strategies. This approach, called an API-first microservices development, focuses first on the client’s needs and then later identifies what API service methods we need to implement for these client requirements.
In our online academic discussion forum app, software functionality such as user sign-up, login, profile management, message posting, and managing post replies are some of the crucial needs we prioritized. In a FastAPI framework, these features are implemented as services using functions that are defined using Python’s def
keyword, with the association of the appropriate HTTP request method through the path operations provided by @app
.
The login
service, which requires username
and password
request parameters from the user, is implemented as a GET
API method:
@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"}
This login service uses bcrypt’s checkpw()
function to check whether the password of the user is valid. Conversely, the sign-up service, which also requires user credentials from the client in the form of request parameters, is created as a POST
API method:
@app.post("/ch01/login/signup") def signup(uname: str, passwd: str): if (uname == None and passwd == None): return {"message": "invalid user"} elif not valid_users.get(uname) == None: return {"message": "user exists"} else: user = User(username=uname, password=passwd) pending_users[uname] = user return user
Among the profile management services, the following update_profile()
service serves as a PUT
API service, which requires the user to use an entirely new model object for profile information replacement and the client’s username to serve as the key:
@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"}
Not all services that carry out updates are PUT
API methods, such as the following update_profile_name()
service, which only requires the user to submit a new first name, last name, and middle initial for partial replacement of a client’s profile. This HTTP request, which is handier and more lightweight than a full-blown PUT
method, only requires a PATCH
action:
@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"}
The last essential HTTP services that we included before building the application are the DELETE
API methods. We use these services to delete records or information given a unique identification, such as username
and a hashed id
. An example is the following delete_post_discussion()
service that allows a user to delete a posted discussion when given a username and the UUID (Universally Unique Identifier) of the posted message:
@app.delete("/ch01/discussion/posts/remove/{username}") def delete_discussion(username: str, id: UUID): if valid_users.get(username) == None: return {"message": "user does not exist"} elif discussion_posts.get(id) == None: return {"message": "post does not exist"} else: del discussion_posts[id] return {"message": "main post deleted"}
All path operations require a unique endpoint URL in the str
format. A good practice is to start all URLs with the same top-level base path, such as /ch01
, and then differ when reaching their respective subdirectories. After running the uvicorn server, we can check and validate whether all our URLs are valid and running by accessing the documentation URL, http://localhost:8000/docs
. This path will show us a OpenAPI dashboard, as shown in Figure 1.2, listing all the API methods created for the application. Discussions on the OpenAPI will be covered in Chapter 9, Utilizing Other Advanced Features.
Figure 1.2 – A Swagger OpenAPI dashboard
After creating the endpoint services, let us scrutinize how FastAPI manages its incoming request body and the outgoing response.