FastAPI Important topics WSGI - Web Server Gateway Interface (WSGI) is a mediator between the web server and python web application. WSGI forwards the requests sent by the users to the web server to the python web applications. In WSGI, applications take a single request and return a response at a time. WSGI does not support async / await, HTTP/2, and web sockets. A simple example of a WSGI application that returns a static “hello world” page is given below. def simple_app(environ, start_response): status = '200 OK' response_headers = [('Content-type','text/plain')] start_response(status, response_headers) return ['Hello world!\n'] ● The above application interface accepts two arguments, environ and start_response. ● environ is a dictionary populated by the server for each request received from the client. ● start_response is a function that takes two arguments; HTTP status code and response header. ● HTTP headers are wrapped in a list of tuples [("Header Name", "Header value")] Status and response headers are sent to the server using the supplied function i.e. start_response. ● Now, the response is returned in a list using the return keyword. WGSI servers - Gunicorn, uWSGI, Apache, etc. WSGI frameworks - Bottle, Flask, Django, etc. ASGI - Asynchronous Server Gateway Interface (ASGI), which is like WSGI is a model made to implement an interface between async-capable python web servers, frameworks, and applications and is applicable across every asyncio framework. A simple example of an application using asynchronous function is given below: async def application(scope, receive, send): event = await receive() ... await send({"type": "websocket.send", ...}) ● The above application interface accepts three arguments; scope, receive and send. ● scope - a dictionary containing information about the incoming connection. ● receive - A channel on which to receive incoming messages from the server. ● send - A channel on which to send outgoing messages to the server. Async - Asynchronous (Async) is a way of handling tasks differently than handling them one by one i.e. multiprocessing at the same time by running the tasks concurrently. While there are a lot of tasks queued to be completed, asynchronous code does not wait for a single task to be completed and move on to the next task rather takes every task into account and carries out another task once the previous task is completed. However, it doesn’t carry out multiple tasks at the same time. Gunicorn - Green Unicorn, often shortened as “Gunicorn” is a pure-python WSGI HTTP server. It is fast, broadly compatible with several python frameworks and applications, and lightweight while running. Uvicorn - Uvicorn is an ASGI server implementation, based on uvloop and httptools and emphasizes speed. It provides support for HTTP/2 and Websockets, which cannot be handled by WSGI. Differences between Uvicorn and Gunicorn The differences between Uvicorn and Gunicorn are as follows: Uvicorn Gunicorn Uvicorn is implemented on ASGI, so it Gunicorn is implemented on WSGI supports asynchronous applications. It supports HTTP/2 and web sockets It does not support HTTP/2 and web sockets It works for the applications that are It does not work for the applications built in asyncio frameworks. that are built in asyncio frameworks. Introduction FastAPI is a high-performance web framework used to build APIs using python. Key features of FastAPI include: ● Fewer bugs. ● Great editor support, short and easy to understand which makes code writing fast. ● Automatic documentation. Why choose Fast API over Django/flask? Async FastAPI is implemented on Asynchronous Server Gateway Interface (ASGI) which supports asynchronous programming and allows us to create both synchronous and asynchronous applications. Type Hints Like other languages like Java and Typescript, python has adapted Type hints since version 3.5. Type hints help catch the errors, autocompletion, improve the readability of the code as well as help in developing and debugging the code that interacts with the API. Being based on type hints, Fast API meets the real standard and is the future. Built-in Documentation In FastAPI, OpenAPI (Swagger) and Redoc are built-in which automatically create interactive API documentation with UI for every endpoint we create. It saves a lot of time and makes debugging relatively easier. For achieving the same task of creating documentation, other frameworks like Django or flask would require other libraries and the hassle of setting up and creating manual documentation. Fast / high-performance Fast API is faster when compared to other python frameworks like Django and flask. It is speed-oriented and can be utilized to build high-performance and scalable applications. Operation/Request Methods GET - To retrieve a page or some information, a GET request is used. POST - A POST method is generally used to add something to a database. For example: Posting a new user login, as the name suggests. PUT - This request method is used to update something already present in the database. DELETE - This request method is used to update information from the database. In addition, there are other operations such as OPTIONS, HEAD, PATCH, AND TRACE. Path Parameters # path parameter @app.get("/get-item/{item_id}") def get_item( item_id: int = Path(None, description="Enter the ID of the item.", gt=0, lt=3) ): return inventory[item_id] ● This function takes a path parameter i.e. {item_id} from the path / endpoint. ● Path is used to set the description and set the constraints, less than (lt) and greater than (gt) to prevent entering invalid IDs. # multiple path parameters @app.get("/get-item/{item_id}/{name}") def get_item(item_id: int, name: str): return inventory[item_id] ● This function takes multiple path parameters i.e. {item_id}, {name} from the path / endpoint. Query Parameters # query parameter @app.get("/get-item-name") def get_item(name: str): for item_id in inventory: if inventory[item_id].name == name: return inventory[item_id] return {"Data": "Not found"} ● If there are no parameters in the path/endpoint, the program interprets the argument passed in the function as a query parameter by default. # optional query parameter @app.get("/get-items") def get_item(name: Optional[str] = None): for item_id in inventory: if inventory[item_id]["Name"] == name: return inventory[item_id] return {"Data": "Not found"} ● If there are no parameters in the path/endpoint, the program assumes that the argument passed in the function is a query parameter by default. # multiple query parameters @app.get("/get-name-test") def get_item(*, name: Optional[str] = None, test: int): for item_id in inventory: if inventory[item_id]["Name"] == name: return inventory[item_id] return {"Data": "Not found"} ● If there are no parameters in the endpoint, the program assumes that it is a query parameter by default. Request Body The request that has to be sent by the user to the API can be declared by using the Pydantic model. ● After importing BaseModel from pydantic, a data model has to be created by declaring the data types. class Item(BaseModel): name: str price: int category: Optional[str] = None ● Then, a parameter has to be passed declaring the data type to point to the pydantic model. @app.post("/create-item") def create_item(item: Item): return item ● FastAPI will automatically do the job of converting the pydantic model object to JSON data and vice-versa. # Request Body + Path Parameter @app.put("/update-item/{item_id}") def add_item(item_id: int, item: Item): if item_id not in inventory: return {"Error": "Item does not exist."} inventory[item_id].update(item) return inventory[item_id] ● Along with the request body, the path parameters can also be passed as shown above. # Request Body + Path Parameter + Query Parameter @app.post("/add-item/{item_id}") def add_item(item_id: int, name: str, item: Item): if item_id in inventory: return {"Error": "Item already exists"} inventory[ item_id ] = item return inventory[item_id], {"Added by": name} ● Also, request body, query parameters, and path parameters can be passed at once. Validation of Parameters Query Parameters Validation of the query parameters can be done using Query. It allows us to add validation, additional information as well as other metadata to the query parameters. ● After importing Query, the default value of the parameter has to be set to Query name: Optional[str] = Query(None). This way different validations can be added to the parameters as shown as follows. # String validation of optional query parameter @app.post("/add-items/{item_id}") def add_item(item_id: int, item: Item, name: Optional[str] = Query(None, min_length=3, max_length=50, regex="staff.")): if item_id in inventory: return {"Error": "Item already exists"} inventory[ item_id ] = item # {"name": item.name, "category": item.category, "price": item.price} return inventory[item_id], {"Added by": name} ● However, validating a “required” parameter will require us to pass us an Ellipsis (...) item_id: int, item: Item, name: str = Query(..., min_length=3) # String validation of required query parameter @app.post("/add-item-name/{item_id}") def add_item(item_id: int, item: Item, name: str = Query(..., min_length=3, max_length=50, regex="staff.")): if item_id in inventory: return {"Error": "Item already exists"} inventory[ item_id ] = item # {"name": item.name, "category": item.category, "price": item.price} return inventory[item_id], {"Added by": name} + ● +-While passing multiple query parameters in a list, Query can be used to set default values for the parameter, name: Optional[List[str]] = Query(["Rock", "Pop"]) # Multiple query parameters / Query parameters list (Default) @app.post("/add-genre") def add_name(name: Optional[List[str]] = Query(["Rock", "Pop"])): name_dic = {"name": name} return name_dic Path Parameters ● Similar to using Query to validate query parameters, path parameters can be validated using Path. Constraints like gt (greater than) and lt (less than )are used to perform a check against the value passed in the parameter. # Path Parameters Validation @app.get("/get-item/{item_id}") def get_item( item_id: int = Path(None, description="Enter the ID of the item.", gt=0, lt=3) ): return inventory[item_id] ● When passing multiple parameters in a function, and we place an optional parameter before a required parameter, it throws an error. To prevent this, we can re-order the parameters and place the required one first or simply add an * before the parameters. @app.get("/get-item/{item_id}/{name}") def get_item(name: str, item_id: int): return inventory[item_id] ● The above function can be written this way by adding an * before the parameters. def get_item(*, name: Optional[str] = None, item_id: int): return inventory[item_id] ● Or, this way by re-ordering the parameters and placing the required ones first. def get_item(item_id: int, name: Optional[str] = None): return inventory[item_id]