Introduction

API stands for Application Programming Interface. An API is a set of subroutine definitions, communication protocols and tools that allows different software applications to communicate with each other. It defines the methods and data formats that applications can use to request and exchange information.
We can think of a API as a power outlet. A socket defines a protocol for its use: the device you want to connect must have two pins, spaced a certain distance apart. If this distance is respected, then the socket will deliver a 220V current at a frequency of 50Hz. From the customer's point of view, i.e. the electrical appliance, it doesn't matter how the electricity is generated. It can be any kind of power plant. From the other side, i.e. from the point of view of the plug, there is no need to adapt to the electrical device: if the protocol is respected, the necessary energy is supplied.
Image credit: @ Arun Kumar Pandey
APIs play a crucial role in modern software development by enabling the integration of different services, systems, or applications. They allow developers to access the functionality of a service or application without needing to understand its internal workings. APIs are used for various purposes, including accessing web services, databases, operating system services, or hardware components.

Some examples: There are many types of APIs possible. Some of them are:

  • the pandas library can actually be seen as an API. There is a set of rules that allow to use complex functions from a simplified documentation.
  • the OpenWeatherMap API is a data API. From an HTTP request, we receive weather data in the requested location, at the requested date.
  • the API Google Cloud Translate allows to use the translation function of Google to easily translate the texts we use.

Types of APIs:

  • Web APIs (HTTP/RESTful APIs): APIs that use the HTTP protocol and follow REST (Representational State Transfer) principles for communication.
  • Library APIs: APIs provided by programming libraries to enable developers to use pre-built functions and classes.
  • Operating System APIs: APIs provided by operating systems to allow applications to interact with the underlying system resources.
  • Database APIs: APIs that provide access to databases, allowing applications to retrieve or manipulate data.

Concepts

Securing an API is crucial to protect sensitive data, maintain the integrity of your system, and prevent unauthorized access. Securing an API rely on three important levels:
  1. Authentication: Authentication is a step that consists in verifying the identity of the client querying the API. By providing some credentials or a key to the API, the server will know that the client is who he claims to be.
  2. Authorization: At the authorization level, the API will check that, based on the identity acknowledged at the previous step, the client is indeed allowed to perform the query.
  3. Traceability: To better the security of your API, you also need to track the queries. We usually store information about What, Who and When the queries were made in a simple log file or, in some cases, in a database.
    • What:
      • which endpoint was called.
      • which method was used.
    • Who:
      • the user that made the call
      • the device used to make that call (mobile phone, web browser, programming language, ...)
      • the IP used by the user
    • When: the time of the request (date and hour)
Image credit: Datascientist.com
Getting information on the source of the request can help add security to your API. For example, you could restrict IP addresses to given countries: if you are a U.S. company with servers, employees and clients in the U.S., a query from somewhere else could be suspicious. Another approach is to validate a few IP addresses that corresponds to the machines using your service and reject any other IP.

Key components

Key components icnclude:
  • Endpoints: URLs or URIs that define where the API can be accessed.
  • Methods: Specify the actions that can be performed, such as GET (retrieve data), POST (submit data), PUT (update data), DELETE (remove data), etc.
  • Request and Response Formats: Define how data is structured when making requests to the API and how it is formatted in the response. Data formats define the structure of the data exchanged between the API and the application.
  • Authentication: Authentication methods ensure that only authorized users can access the API's functionality.

For example: the following URI http://example.org/resource can be read as follows:

  • http:// is the protocol used
  • example.org is the domain name of the server, which is a simplification of the server's IP address.
  • /resource is the endpoint you wish to request
Image credit: Arun Kumar Pandey

Note that a URI can also have parameters. For example, the URI http://example.org/resource?key1=value1&key2=value2 allows to pass the keys key1 and key2 which have respectively the values value1 and value2 to the server. This is called a query string or query parameters.

HTTP responses

When an HTTP request is sent, the server sends a response to the client. This response is also composed of different elements:
  • Headers with the response metadata
  • A body with the content of the response
  • A status code
The content of an HTTP response is very often HTML for websites but it is more likely to be JSON or XML for APIs. The status code makes it easy to understand if the request was successful. By convention, the error codes should correspond to the following states:
  • 10X: Information
  • 20X: Means that the request was successful
  • 30X: Redirection
  • 40X: Means a client-side error / Client error
  • 50X: Means server side error / Server error
Thus, a 404 code is an error from the client that did not enter the right address to access the resource while a 503 error will be an error from the server that cannot run the requested service. Websites work on the same principle of server-client architecture that we request via HTTP using a Web browser. Thus, by clicking on a link, the browser sends a request of type GET to the server of the website. If the address is correct, the server returns a response containing an HTML file that will be interpreted by the browser. Similalry, when a form is filled in on a website, it is generally a request of type POST. The request then contains the data filled in the form.
HTTPS protocol
The HTTPS protocol is a more secure version of the HTTP protocol. It is in fact the HTTP protocol to which an SSL (Secure Socket Layer) encryption layer is added. It protects the authentication of a server, the confidentiality and integrity of the data exchanged, and sometimes the authentication of the client: a public key is given to the client so that the data sent back to the server is encrypted; this data is then decoded thanks to a private key available on the server. It tends to become the standard, pushed by search engines that better reference sites using an HTTPS protocol.
HTTP clients
As we have seen in the previous examples, the browser is a client that allows to make HTTP requests to servers that are able to return data according to the request. However, there are other simpler tools to use when you want to interact with an API.
CURL
We will query an API from the terminal using the command line interface cURL (client URL Request Library). To make an HTTP request from the terminal with cURL, the syntax for making a request is as follows :
curl -X GET http://example.com 
The -X argument introduces the method, in this case, GET. Then we can write the URI.
  • For example:
    curl -X GET https://jsonplaceholder.typicode.com/posts/1

    With this request, we receive a "stringified" JSON object containing information about a post.

    Image credit: Arun Kumar Pandey

    Here, we have requested the post with ID 1. We can get all posts with a GET request to the /posts endpoint. This is a case we commonly encounter: the endpoint with an ID returns an individual observation while without any ID it returns all observations.

    The HTTP response to this request contains only the body. To see the headers, we can add the -i argument to the :

    curl -X GET -i https://jsonplaceholder.typicode.com/posts/1
    In the header, you can see information about the content of the response, such as the status code, which in this case is 200.
  • We have seen how to make a request with the GET method. Now, if you want to use the PUT method to add data to the API, you will probably want to specify a body and headers. To do this, you need to precede your headers with the -H argument and the body with the -d argument.
    
                  curl -X PUT -i\
                  -H "Content-Type: application/json"\
                  -d '{"id": 1, "content": "hello world"}'\
                  https://jsonplaceholder.typicode.com/posts/1
                
    Here, we indicate in the header the type of data sent (JSON), and in the body the data in question. cURL is undoubtedly the simplest tool to use.
Postman
Postman is a collaborative platform for API development. It allows to build and execute HTTP requests, to store them in a history so that they can be replayed, and to organize them in collections. Among other things, Postman allows us to:
  • Quickly and easily send REST, SOAP and GraphQL requests
  • Automate manual tests and integrate them into a CI/CD pipeline to ensure that no code changes interrupt the API in production.
  • Communicate the expected behavior of an API by simulating endpoints and their responses without having to configure a backend server
  • Generate and publish nice machine-readable documentation to make the API easier to use.
  • Stay up-to-date on the health of your API by checking performance and response times at scheduled intervals.
  • Collaborate in real time with built-in version control.
Postman offers a very simple interface to make complex HTTP requests. It is a very useful tool for testing/exploring an API. Moreover, Postman's features allow us to easily export a request made with Postman in the language of our choice.

Python HTTP libraries

The simplest library to make requests with Python is probably the Requests library and it can be installed using:

          pip3 install requests
          # or conda install requests
        
and then we can access the content as:

          import requests
          # creating a GET request
          r = requests.get('https://jsonplaceholder.typicode.com/posts/1')
          # getting the response elements
          response_dict = r.json
          response_header = r.headers
          status_code = r.status_code
        
We can of course pass a body, headers with this library.

          r = requests.put(
            url='https://jsonplaceholder.typicode.com/posts/1',
            data={"id": 1, "content": "hello world"},
            headers={"Content-Type": "application/json"}
            )
        
Another popular library is the Urllib library installed natively with Python but more difficult to use.

The REST standard

Each API has a specific architecture, as well as rules to respect that determine the data formats and commands accepted to communicate with. In order to promote accessibility and standardization of APIs for developers, there are now classic API architectures that are very often used. For example, REST (Representational State Transfer) is an architecture that is very often used in the creation of WEB services. It allows applications to communicate with each other regardless of the operating system via the HTTP protocol. A REST API uses HTTP requests to communicate and must respect the following principles:
  • Client-server architecture : the client must make HTTP requests to request resources. There is independence between the client side and the server application so that changes to one endpoint do not affect the others.
  • Uniform interface : simplified architecture that allows each part to evolve independently. To speak of a uniform interface, 4 constraints must be respected
  • Resource identification in requests : resources are identified in requests and are separated from the representations returned to the client.
  • Resource manipulation by representations : clients receive files that represent resources. These representations must contain enough information to be modified or deleted.
  • Self-describing messages : all messages returned to the client contain enough information to describe how the client should handle the information.
  • Hypermedia as an engine for application state change (HATEOAS) : after accessing a resource, the REST client must be able to discover all other available actions through hyperlinks.
  • Cached : the client must be able to cache the data that the API provides in response, you can check this link for further informations.
  • Layered system : communication can take place through intermediate servers (proxy servers or load balancing devices).
  • Stateless : no information is stored between two requests and the API treats each request as a first request.
Note that in the REST standard, the endpoint must designate an object that we wish to manipulate and the methods must correspond to the following effects
  • GET to retrieve information
  • POST to add new data to the server
  • PUT to modify data already on the server
  • DELETE to delete data
This standard is not the only one and is not mandatory to implement to get an efficient API but it is probably the most known.

FastAPI

FastApi is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. It is designed to be easy to use, fast to develop with, and provide high performance, making it an excellent choice for building RESTful APIs. Here's a brief overview and key features of FastAPI:
  1. Fast: As the name suggests, FastAPI is designed to be high-performance. It is built on top of Starlette and Pydantic, which contribute to its speed.
  2. Type Annotations: One of the standout features is the use of Python type hints to define the types of request and response data. This not only serves as documentation but also enables automatic data validation and serialization.
  3. Automatic Documentation: FastAPI generates OpenAPI and JSON Schema documentation automatically based on your Python type hints. You can explore and interact with your API using Swagger UI or ReDoc.
  4. Asynchronous Support: FastAPI fully supports asynchronous programming using Python's async and await keywords. This allows for efficient handling of I/O-bound operations.
  5. Dependency Injection System: FastAPI has a built-in dependency injection system that makes it easy to manage dependencies and share common resources across different parts of your application.
  6. OAuth2 and JWT Authentication: It provides built-in support for OAuth2 and JWT authentication, making it easier to secure your APIs.
  7. Data Validation and Serialization: FastAPI uses Pydantic models for data validation and serialization. This allows you to define data models with type hints, and FastAPI will automatically handle validation and serialization/deserialization.
  8. WebSocket Support: In addition to traditional HTTP APIs, FastAPI supports WebSocket communication, allowing for real-time bidirectional communication.
  9. Cors (Cross-Origin Resource Sharing) Middleware: FastAPI includes middleware for handling Cross-Origin Resource Sharing, making it easy to control which domains can access your API.
  10. Dependency Injection System: FastAPI has a sophisticated dependency injection system that simplifies handling dependencies in your API, making your code clean and modular.
Gettign started:
In this section we will look at the basics of FastAPI. The first step is to install the fastapi and uvicorn libraries. uvicorn is a library that allows you to launch the server created by FastAPI.
The Python libraries for creating APIs generally use another server to launch the API. For example, you can launch a Flask API without uvicorn but it is generally not recommended (see the launch message).
pip install fastapi uvicorn
Here's a simple example of a FastAPI application (create main.py file):

            from fastapi import FastAPI

            app = FastAPI()
            
            @app.get("/")
            def read_root():
              return {"Hello": "World"}
            
            @app.get("/items/{item_id}")
            def read_item(item_id: int, query_param: str = None):
                return {"item_id": item_id, "query_param": query_param}            
        
We can run the app with Uvicorn:
uvicorn main:app --reload
Visit https://127.0.0.1:8000 in your browser, and you should see the automatically generated Swagger documentation. In the conolse, the folloing line should be observed:
INFO: Uvicorn running on http://127.0.0.1:8000(Press CTRL+C to quit)
This line gives us the address at which the API works.

In another console, issue the following command to query the endpoint /:

curl -X GET http://127.0.0.1:8000/   

In this section we will see how to use dynamic routing and then how to pass arguments directly into a request. Finally, we will see how FastAPI generates documentation for arguments.

  1. Dynamic Routing: Dynamic routing allows you to generate endpoints automatically. Modify the main.py file by pasting the following lines :
    
                from fastapi import FastAPI
                users_db = [
                    {
                        'user_id': 1,
                        'name': 'Alice',
                        'subscription': 'free tier'
                    },
                    {
                        'user_id': 2,
                        'name': 'Bob',
                        'subscription': 'premium tier'
                    },
                    {
                        'user_id': 3,
                        'name': 'Clementine',
                        'subscription': 'free tier'
                    }
                ]
                app = FastAPI()
                @app.get('/')
                def get_index():
                    return {
                        'greetings': 'welcome'
                    }
                @app.get('/users')
                def get_users():
                    return users_db
                @app.get('/users/{userid:int}')
                def get_user(userid):
                    try:
                        user = list(filter(lambda x: x.get('user_id') == userid, users_db))[0]
                        return user
                    except IndexError:
                        return {}
                @app.get('/users/{userid:int}/name')
                def get_user_name(userid):
                    try:
                        user = list(filter(lambda x: x.get('user_id') == userid, users_db))[0]
                        return {'name': user['name']}
                    except IndexError:
                        return {}
                @app.get('/users/{userid:int}/subscription')
                def get_user_suscription(userid):
                    try:
                        user = list(filter(lambda x: x.get('user_id') == userid, users_db))[0]
                        return {'subscription': user['subscription']}
                    except IndexError:
                        return {}
              

    Here users_db is the database and with this code, we want to:

    • GET / returns a welcome message
    • GET /users returns the entire database
    • GET /users/userid returns all the data for a user based on its id. userid should be an integer. If the userid provided does not match an existing user, an empty dictionary will be returned.
    • GET /users/userid/name returns the name of a user based on its id. userid should be an integer. If the userid provided does not match an existing user, an empty dictionary will be returned.
    • GET /users/userid/subscription returns the subscription type of a user based on their id. userid should be an integer

    We notice that the interface to use these requests allows to pass arguments for dynamic routing :

    We can see below, how dynamic routing works here:

    We need to provide the entries to get the output.
  2. Request string: We have seen how to pass data to the API using dynamic routing. In this part, we will see how to do it using the query string. FastAPI makes it easy to specify which arguments can be passed via the query string. In the following example, we will define a function that can take an argument argument1. This argument must be passed in the query string.
    
                from fastapi import FastAPI
                api = FastAPI()
                @api.get('/')
                def get_index(argument1):
                    return {
                        'data': argument1
                    }
              

    When using the curl command in the command prompt, we can interact with web servers by sending HTTP requests. Let's break down two scenarios:

    We need to provide the entries to get the output.

    In the first case, with the command, we're sending a GET request (-X GET) to http://127.0.0.1:8000/ with an additional query parameter argument1 set to hello world. The server processes this request and returns the value of the argument1 parameter, which in this case is "hello world" (in first case). Whereas in second case, we're sending a GET request to the same server but without any query parameters. However, the server expects the argument1 parameter to be present. Since it's missing, the server responds with a 422 Unprocessable Entity error. This error indicates that the request couldn't be processed because the required field (argument1) is missing.

    We can see this beahviour through the docs/ documentation. We need to provide the argument otherwise, when executed it will produce an error. We see in the interface that the argument1 field is required. Going down the interface, we can also see the codes that this route can return: 200 if the request succeeds and 422 if the request does not have the required field.

    • Another example:
      
                      @app.get('/typed')
                      def get_typed(argument1: int):
                          return {
                              'data': argument1 + 1
                          }
                    
      This basically gives error, if provide not an integer as argument. However, when provided a strong, it gives our output as {"data" : 123} when argument1 = 123
      We need to provide an integer entry to get the output.
    • Example on Optional class: Finally, we can choose to have an optional argument. For this we can use the Optional class of the typing library. However, a default value must be provided.
      
                      from typing import Optional
                      @api.get('/addition')
                      def get_addition(a: int, b: Optional[int]=None):
                          if b:
                              result = a + b
                          else:
                              result = a + 1
                          return {
                              'addition_result': result
                          }
                    
      where the function takes two parameters: 'a' (an integer) and 'b' (an optional integer), with a default value of 'None'. If 'b' has a value (i.e., it's not 'None'), 'a' and 'b' are added together, and the result is assigned to the variable result. If 'b' is None, 'a' is incremented by '1', and the result is assigned to result.
  3. Request body: To pass data to the API, the FastAPI library relies on the use of the BaseModel class from pydantic to specify the form of the query body. We will first create an Item class inherited from the BaseModel class.
    
                  from fastapi import FastAPI
                  from typing import Optional
                  from pydantic import BaseModel
                  
                  class Item(BaseModel):
                      itemid: int
                      description: str
                      owner: Optional[str] = None
                  
                  app = FastAPI()
                  
                  @app.post('/item')
                  def post_item(item: Item):
                      return {
                          'itemid': item.itemid
                      }
                

    Here the Item class has the attributes itemid which must be an integer, description which must be a string and owner which is an optional string. We are going to create a route for which we will have to associate a query body containing these attributes. We can open the OpenAPI and inspect the route description POST /item:

    We need to provide an 'Request body' to get the output.
    We can see that the annotations given here are no longer counted as parameters (arguments to be specified in the query string). On the other hand, we must now specify the request body ("Request body required"). Thanks to the inheritance of the BaseModel class, we could change the interpretation of the annotation by FastAPI. The use of this BaseModel class as a parent class thus allows a route to accept a body. We force the body of the request to respect a certain schema with certain values that can be optional. Moreover, the use of this class allows to ignore fields which are not predefined. Finally, the BaseModel class allows to easily return all the attributes of the body of a query that have been created in JSON format without having to specify this definition. Note that we can use the typing and pydantic libraries to give more complex types to our data. The following example shows a use of these libraries:
    
                  from pydantic import BaseModel
                  from typing import Optional, List
                  class Owner(BaseModel):
                      name: str
                      address: str
                  class Item(BaseModel):
                      itemid: int
                      description: str
                      owner: Optional[Owner] = None
                      ratings: List[float]
                      available: bool
                
    Pydantic also allows to use "exotic" types like http URLs, IP addresses, ... If you want to explore these data types, you can go to this https://docs.pydantic.dev/latest/concepts/types/.
  4. Headers: In this part, we will see how to pass data to the server via the request headers. This command can be very useful to pass authentication tokens or to check the content type, the origin of the request, ... For this, we can use the Header class of fastapi. For example, the following function checks the value of User-Agent. This header is used to determine the source of a request :
    
                  from fastapi import Header
                  @api.get('/headers')
                  def get_headers(user_agent=Header(None)):
                      return {
                          'User-Agent': user_agent
                      }
                
    We see in this case that the returned user-agent is the browser user-agent.

Example Fastapi

In the above case-1 (Dynamic Routing), we have seen the users_db. Now we want to add folloing routes:
    PUT /users creates a new user in the database and returns the data for the created user. The data about the new user must be provided in the body of the query. POST /users/userid modifies the data for the user identified by userid and returns the data for the modified user. The data about the user to be modified must be supplied in the body of the request DELETE /users/userid deletes the user identified by userid and returns a confirmation of the deletion.
We will choose to return an empty dictionary in the case of an internal error and we will use a User class inherited from BaseModel.
We need to provide an integer entry to get the output.
The correspodning main.py file is:

          from fastapi import FastAPI
          from typing import Optional
          from pydantic import BaseModel
          
          users_db = [
              {
                  'user_id': 1,
                  'name': 'Alice',
                  'subscription': 'free tier'
              },
              {
                  'user_id': 2,
                  'name': 'Bob',
                  'subscription': 'premium tier'
              },
              {
                  'user_id': 3,
                  'name': 'Clementine',
                  'subscription': 'free tier'
              }
          ]
          app = FastAPI()
          class User(BaseModel):
              userid: Optional[int]
              name: str
              subscription: str
          @app.put('/users')
          def put_users(user: User):
              new_id = max(users_db, key=lambda u: u.get('user_id'))['user_id']
              new_user = {
                  'user_id': new_id + 1,
                  'name': user.name,
                  'subscription': user.subscription
              }
              users_db.append(new_user)
              return new_user
          @app.post('/users/{userid:int}')
          def post_users(user: User, userid):
              try:
                  old_user = list(
                      filter(lambda x: x.get('user_id') == userid, users_db)
                      )[0]
                  users_db.remove(old_user)
                  old_user['name'] = user.name
                  old_user['subscription'] = user.subscription
                  users_db.append(old_user)
                  return old_user
              except IndexError:
                  return {}
          @app.delete('/users/{userid:int}')
          def delete_users(userid):
              try:
                  old_user = list(
                      filter(lambda x: x.get('user_id') == userid, users_db)
                      )[0]
                  users_db.remove(old_user)
                  return {
                      'userid': userid,
                      'deleted': True
                      }
              except IndexError:
                  return {}
        
Conclusion

FastAPI is a powerful and easy-to-use framework for building APIs with Python. Its combination of type hinting, automatic documentation, and high performance makes it a strong choice for a wide range of API development projects. Whether you're building a small RESTful API or a complex real-time application, FastAPI provides the tools and features to make development efficient and enjoyable.

FastApi documentation

  • Customize the documentation of functions: To add comments to the use of an endpoint, we can use the docstring of the function. We can also give a name to our API via the FastAPI class.
    
                    from fastapi import FastAPI
        
                    app = FastAPI(
                      title="My API",
                      description="My own API powered by FastAPI. Created by Arun Kumar Pandey.",
                      version="1.0.1")
                    
                    @app.get("/")
                    def read_root():
                      """Returns greetings
                      """
                      return {"Hello": "World"}         
                
    • If we provide description of the function as a docstring, the interface automatically shows the description straight from the docstring of the function. We can also add the name argument to the decorator to name the route in OpenAPI: by default, route names are created from the titles of the functions used: Get Index for read_root. For example, we can set decorator name
      
                          @api.get('/', name="Hello World")
                          def get_index():
                              """Returns greetings
                              """
                              return {'greetings': 'welcome'}
                        
  • Documentation of the function body: We will now see how FastAPI handles the definition of models from the BaseModel class:
    
                    from pydantic import BaseModel
                    from typing import Optional
                    class Computer(BaseModel):
                        computerid: int
                        cpu: Optional[str]
                        gpu: Optional[str]
                        price: float
                    
                    @app.put('/computer', name='Create a new computer')
                    def get_computer(computer: Computer):
                        """Creates a new computer within the database
                        """
                        return computer
                  
    In the Schemas tab, the description of the Computer class is now available. Here we have also added description on the headers. You can see in the function description that the header is documented.
  • Organizing the documentation: By default, all functions are documented in a default tab. However, you can choose to organize these functions in different parts. To do this, one must specify the tags argument in the decorator.
    
                    app = FastAPI(
                      title="My FastApi",
                      description="My own API powered by FastAPI. Created by Arun Kumar Pandey.",
                      version="1.0.1",  
                      openapi_tags=[
                          {
                              'name': 'home',
                              'description': 'default functions'
                          },
                          {
                              'name': 'items',
                              'description': 'functions that are used to deal with items'
                          }
                      ]
                  )
                  
                  @app.get('/', tags=['home'])
                  def get_index():
                      """returns greetings
                      """
                      return {
                          'greetings': 'hello world'
                      }
                  
                  @app.get('/items', tags=['home', 'items'])
                  def get_items():
                      """returns an item
                      """
                      return {
                          'item': "some item"
                      }              
                  
    Now note down the various parts added here.
    The functions are now divided into different parts. The same function can be put in several parts. We could add a description for the different parts using the openapi_tags argument of the FastAPI class constructor. We can also change the address of the OpenAPI and Redoc documentations using the docs_url or redoc_url arguments. If these arguments are set to None, these endpoints are disabled. Finally, one can choose to change the address of the OpenAPI manifest with the openapi_url argument.
    
                    app = FastAPI(
                      # Change the address of the OpenAPI documentation
                      docs_url="/custom-docs",  # Set to the desired URL path, e.g., "/custom-docs"
                      
                      # Change the address of the ReDoc documentation
                      redoc_url=None,  # Set to None to disable the ReDoc endpoint or specify a custom URL
                      
                      # Change the address of the OpenAPI manifest
                      openapi_url="/custom-openapi"  # Set to the desired URL path, e.g., "/custom-openapi"
                  )
                  
  • Using errors: Errors are an important tool when developing applications: they allow to easily give indications about a bad handling of the application. In particular, we can give information by using the status code of the response but also by giving information in the body of the response. Up to now, we have been able to obtain errors of types 500, 404 and 422. In this part we will see how to create more custom errors.
  • 
                  app = FastAPI()
                  data = [1, 2, 3, 4, 5]
                  @app.get('/data')
                  def get_data(index):
                      return {
                          'data': data[int(index)]
                      }
                

    If we open the OpenAPI documentation, we can see that two errors are proposed. In this case, we can see that if the value of index is greater than 4 or less than 0, we may get an IndexError. Also, if index is not an integer, we should get a ValueError.

    We have also seen that FastAPI generates its own errors for routes not found (404: you can make a GET /nowhere request to see for yourself) or for data formats that do not correspond to the defined expectations (422: in particular via the use of annotations or classes inherited from BaseModel). We will use try-except blocks to catch Python errors and return HTTPException with the associated HTTP codes.

    
                  from fastapi import HTTPException
                  app =FastAPI()
                  @app.get('/data')
                  def get_data(index):
                      try:
                          return {
                              'data': data[int(index)]
                          }
                      except IndexError:
                          raise HTTPException(
                              status_code=404,
                              detail='Unknown Index')
                      except ValueError:
                          raise HTTPException(
                              status_code=400,
                              detail='Bad Type'
                          )
                

    So we can easily change the error codes and the data returned on error. For the detail argument, we can give a dictionary or any other structure that can be interpreted as a JSON.

    Finally, if we want to change the form of the data returned on error, we can create our own exceptions and pass them to the @api.exception_handler decorator.

    
                  from fastapi import FastAPI
                  from fastapi import Request
                  from fastapi.responses import JSONResponse
                  import datetime
                  
                  app = FastAPI()
                  class MyException(Exception):
                      def __init__(self,                 
                                   name : str,
                                   date: str):
                          self.name = name
                          self.date = date
                  @app.exception_handler(MyException)
                  def MyExceptionHandler(
                      request: Request,
                      exception: MyException
                      ):
                      return JSONResponse(
                          status_code=418,
                          content={
                              'url': str(request.url),
                              'name': exception.name,
                              'message': 'This error is my own', 
                              'date': exception.date
                          }
                      )
                  @app.get('/my_custom_exception')
                  def get_my_custom_exception():
                      raise MyException(
                        name='my error',
                        date=str(datetime.datetime.now())
                        )
                

    Let's take some time to describe this code. In the first block, we define a new Exception. We give it the attributes name and date. In the second block, we tell FastAPI how to react when the exception is raised. We give a response of type JSON to return, giving it a status_code and a status. We can then access the attributes of the request or the exception to return them in a JSON. Finally the last block allows us to define a route that generates this error.


JSON: JavaScript Object Notation

JSON, or JavaScript Object Notation, is a lightweight data-interchange format. It is easy for humans to read and write and easy for machines to parse and generate. It is widely used for data exchange between a server and a web application (i.e. is used extensively in web applications and APIs), as well as for configuration files and data storage.

Basics of JSON
  1. Data Structure:
    • JSON represents data as key-value pairs, similar to a dictionary in Python or an object in JavaScript.
    • Data is organized in name/value pairs, where the name (a string) is followed by a colon, and the value can be a string, number, boolean, array, or another JSON object.
    • 
                              {
                                  "name": "John Doe",
                                  "age": 30,
                                  "isStudent": false,
                                  "courses": ["Math", "History", "Science"],
                                  "address": {
                                    "street": "123 Main St",
                                    "city": "Anytown",
                                    "zip": "12345"
                                  }
                                }
                          
  2. Data Types: JSON data is organized into key-value pairs. Each key-value pair is enclosed in curly braces {}. The key is a string of characters and the value can be any of the following types:
    • Strings: Enclosed in double quotes.
    • Numbers: Integer or floating-point.
    • Booleans: true or false.
    • Arrays: Ordered list of values.
    • null: A special value that indicates the absence of a value.
    • Objects: Unordered collection of key/value pairs.
  3. JSON vs. JavaScript Object: While JSON syntax resembles JavaScript object syntax, they are not exactly the same. In JSON, keys must be strings, and strings must be enclosed in double quotes.
Use cases:
  1. Data Exchange: Commonly used for sending and receiving data between a server and a web application. APIs often return data in JSON format.
  2. Configuration Files: Used for configuration settings in various applications. Easy to read and write manually.
  3. Storage: NoSQL databases often use JSON-like formats to store data.
  4. Serialization: Objects in programming languages can be serialized into JSON for data interchange.
JSON Schema:
JSON Schema is a vocabulary that allows you to annotate and validate JSON documents. It provides a way to describe the structure of JSON data for documentation, validation, and interaction.

For creating JSON schema from scratch, see the Create a nested data structure.

Example: the expected structure and constraints for a JSON object representing information about an individual, particularly someone's personal details, such as a student. Let's break down the schema:
  1. Person Details:
    • The root object represents details about an individual, likely a person.
  2. Name:
    • The "name" property is expected to be a string, representing the person's name.
  3. Age:
    • The "age" property is expected to be an integer, representing the person's age.
  4. Student Indicator:
    • The "isStudent" property is a boolean indicating whether the person is a student or not.
  5. Courses:
    • The "courses" property is an array of strings, presumably representing the courses the student is enrolled in.
  6. Address:
    • The "address" property is an object with details about the person's address.
  7. Address Details:
    • It includes "street," "city," and "zip" properties, all of which are expected to be strings.
    • These address details are marked as required.
  8. Overall Requirements:
    • The entire object must have the "name," "age," and "isStudent" properties.
    • The "address" property is optional, but if present, it must include the required details ("street," "city," and "zip").

            {
                "$schema": "http://json-schema.org/draft-07/schema#",
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "age": {
                    "type": "integer"
                  },
                  "isStudent": {
                    "type": "boolean"
                  },
                  "courses": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "address": {
                    "type": "object",
                    "properties": {
                      "street": {
                        "type": "string"
                      },
                      "city": {
                        "type": "string"
                      },
                      "zip": {
                        "type": "string"
                      }
                    },
                    "required": ["street", "city", "zip"]
                  }
                },
                "required": ["name", "age", "isStudent"]
              }
        
This schema defines the expected structure and types of the JSON object. This can be understood as:

            object
            ├─ name: string
            ├─ age: integer
            ├─ isStudent: boolean
            ├─ courses: array
            │   └─ items: string
            └─ address: object
                ├─ street: string
                ├─ city: string
                └─ zip: string            
        
This schema could be used, for example, in a system where user details need to be validated before they are stored or processed. It ensures that the data conforms to a specific structure and type, enhancing data integrity and providing a basis for consistent data exchange.

Conclusion:
JSON is a versatile and widely adopted format for data interchange. Its simplicity, human-readability, and ease of integration make it a popular choice for APIs, configuration files, and data storage. Understanding JSON is essential for anyone working with web development, APIs, or data exchange between different systems.

References


Some other interesting things to know: