Lab 1: Back end (Python)

In this lab we will build a simple web server using Python and the framework Flask. We will also introduce the concepts of REST and JSON and create a database. Using these you will create a server containing an API that communicates with JSON over HTTP and follows the REST style. Your API should ultimately contain the following routes:

  • GET, POST /cars

  • GET, PUT, DELETE /cars/[int:car_id]

  • GET, POST /customers

  • GET, PUT, DELETE /customers/[int:customer_id]

  • GET /customers/[int:customer_id]/cars

Initially we will use Postman to handle HTTP communication to and from our server.

Remember to continuously make git commits; a recommended minimum interval is to create a commit for each part of the lab.

HTTP (Hypertext Transfer Protocol)

Before we begin we will need to know more about communication over the internet and specifically how this is done via HTTP. Read at least the following article but we encourage you to find your own articles related to HTTP if the article above has not created a basic understanding. It is important to understand:

  • The different methods GET, POST, PUT and DELETE and how they are used.

  • Headers (Specifikt inklusive “content-type”)

  • Body

  • Status codes

Postman

In order to simplify the process of sending and receiving HTTP messages we will use a program that helps us with this. Download and install Postman from this page and then read through an article that goes through how Postman can be used.

REST

Det finns olika sätt att strukturera kommunikation med en webbserver, men den stil vi använder i denna kurs kallas REST och står för Representational State Transfer. REST innebär i korta drag att man definierar resurser (exempelvis en “User”) som kan nås via specifika URIs. En URI i kombination med någon utav HTTP-metoderna (GET, POST, PUT, DELETE) utgör ett kommando. Läs mer om REST och skumma igenom dokumentationen.

JSON

Data can be structured in different ways; we use the JSON format because it is easy to understand and convenient to handle with both JavaScript and Python.

Preparatory questions

Answer the following questions and write down your answers before the demonstration:

  1. For what types of operations should the different HTTP verbs be used? Give a concrete example of suitable functionality for each verb.

  2. Give examples of at least four status codes and when they should be used.

  3. In which part of an HTTP request can data in JSON format be found?

Part 1: Routing

  1. Start by following Starting a new lab to create a new branch for the lab named lab1. Note that it is possible to start the lab before the previous one is approved.

  2. Create the route /cars in your main.py that returns a list of cars in JSON format. The list of cars can for now be hard-coded according to the following Python code:

    car_list = [
      {"id": 1, "make": "Volvo", "model": "V70"},
      {"id": 2, "make": "SAAB", "model": "95"}
    ]
    

    A route is defined by defining a method that is decorated with app.route.

    An example of a suitable declaration for /cars can look like this:
    @app.route('/cars')
    def cars():
      # ...
    

    Tip

    Python dictionaries and common data types can be automatically converted to JSON format by Flask; other data types can be converted by using jsonify. Note that lists may not exist at the top level of what is returned, but jsonify can be used to return lists.

    Tip

    You need to restart the Flask application when you have made changes to the code for them to be applied. If you want the server to automatically reload changes when files are saved, pass debug=True as a parameter to app.run().

    When you call the server from Postman the response should look like the following: lab1-1-13

    If there are no cars an empty list should be presented.

  3. Create a route /cars with a parameter to retrieve a specific car from the car_list based on its id. This route should have the following format: /cars/[int:car_id]

    Tip

    Information about routes and parameters in Flask.

    If you do not include information about what type the parameter should have, Flask assumes it has the type string (and this can cause follow-on errors).

    If the requested car is found the response from the server should look like this:

    lab1-1-2 You should also include simple error handling. In this case it means that if a car cannot be found the HTTP status code 404 Not Found should be shown to indicate that the resource could not be found. The response should then look like this:

    _images/lab1-1-21.png

    Tip

    To respond with only an HTTP status code in Flask you can use the abort() method, see the following article for more information and examples.

Part 2: Database

In most web applications some way of storing data over time is needed. This is usually handled by using a database. In this lab we will use the database SQLite. SQLite is a database that is very easy to get started with, but should not be used this way in a production environment. To more easily manage the database we will also use SQLAlchemy. SQLAlchemy contains a so-called ORM (Object Relational Mapper) that helps us map our future Python classes to database tables. More in-depth documentation on SQLAlchemy can be found here.

We will now arrange for cars to be saved in a database instead of consisting of a hard-coded Python list.

  1. Install Flask-SQLAlchemy (SQLAlchemy + integration with Flask) by running the following command (in your virtual Python environment): (venv)$ pip install flask_sqlalchemy

    Tip

    To more easily manage packages in the future you can create a file, server/requirements.txt, with similar content (this is only a brief example of what it can look like):

    Flask==2.0.2
    Flask-SQLAlchemy==2.5.1
    SQLAlchemy==1.4.31
    

    Then you just run the following to install all packages at once:

    (venv)$ pip install -r requirements.txt
    

    You can also use pip to automatically create this file, which will also include packages that depend on flask and flask_sqlalchemy. This is done with the command:

    (venv)$ pip freeze >> requirements.txt
    
  2. Add the following configuration below the assignment of the app variable in main.py:

    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    db = SQLAlchemy(app)
    

    You also need to import SQLAlchemy:

    from flask_sqlalchemy import SQLAlchemy
    

    The database will then consist of the file database.db which we do not want to version control. Therefore add to your .gitignore:

    # Database
    server/database.db
    
  3. Det är nu dags att skapa vår första modell, “Car”. En modell skapas genom att skapa en klass som ärver av db.Model enligt följande:

    class Car(db.Model):
      id = db.Column(db.Integer, primary_key=True)
      make = db.Column(db.String, nullable=False)
      model = db.Column(db.String, nullable=False)
    
      def __repr__(self):
        return '<Car {}: {} {}'.format(self.id, self.make, self.model)
    

    As shown in the example a model can, just like usual, contain arbitrary methods and attributes just like a normal class in Python. Thanks to inheriting from SQLAlchemy’s db.Model many methods are also available automatically in the class, for example Car.query.all() which returns all car instances in the database. db.Column indicates that the attribute should be mapped to a column in the database.

    The model above will be represented by a table in the database with the fields id, make and model, as shown in the following image.

    (venv) ~/tddd83-labs/server$ sqlite3 database.db -header -column
    SQLite version 3.22.0 2018-01-22 18:45:57
    Enter ".help" for usage hints.
    sqlite> SELECT * FROM car;
    id       make      model
    -------  --------  ----------
    1        Volvo     V70
    2        SAAB      95
    
  4. Now remove your hard-coded cars, i.e. cars_list, entirely.

  5. It is now time to test manipulating the database. We start by doing it through the Python interpreter:

    Start Python with your virtual environment activated:

    (venv) ~/tddd83-labs/server$ python
    Python 3.8.10 (default, Nov 26 2021, 20:14:08)
    [GCC 9.3.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>>
    

    Now import db and your new model, Car, from main.py by writing:

    >>> from main import db, Car
    

    Let SQLAlchemy create tables in the database by running the following:

    >>> db.create_all()
    

    To save a new car in the database you simply create an instance of Car and save it in the database like this:

    >>> volvo = Car(make="Volvo", model="V70")
    >>> db.session.add(volvo)
    >>> db.session.commit()
    

    The id field is handled automatically since it is defined as a primary key.

    To then retrieve all cars in the database you use for example:

    >>> Car.query.all()
    [<Car 1: Volvo V70, <Car 2: SAAB 95]
    

    More information about how to retrieve, add and delete data can be found in the documentation for SQLAlchemy.

  6. Now you can finally use the database when your API responds to HTTP requests! Rewrite your methods for /cars and /cars/[int:car_id] so that they work exactly as before, but retrieve all data from the database.

    Tip

    Note that jsonify cannot handle arbitrary data types and that you need to somehow convert a Car instance to a Python dictionary so that it can then be converted to JSON. A good start may be to add a method to the Car class so that you get something along the lines of this:

    class Car(db.Model):
      id = db.Column(db.Integer, primary_key=True)
      make = db.Column(db.String, nullable=False)
      model = db.Column(db.String, nullable=False)
    
      def __repr__(self):
        return '<Car {}: {} {}'.format(self.id, self.make, self.model)
    
      def serialize(self):
        return dict(id=self.id, make=self.make, model=self.model)
    

Part 3: Support for more HTTP methods

Displaying data on request is certainly very good, but it is also very practical if users can add, update and delete data themselves via your API!

To enable this you will now implement functionality to also support the HTTP methods POST, PUT and DELETE.

  1. Extend the method for the /cars route so that it also accepts POST requests. For GET the method should work exactly as before.
    For POST a new car should be added to the database with the make and model specified in the request. The request should, in Postman, look like this:
    _images/lab1-3-1.png

    The response to the POST request should consist of the new car object (in the same way as /cars/[int:car_id]), i.e.:

    {
        "id": 1,
        "make": "Audi",
        "model": "A6"
    }
    

    Read about how to handle different HTTP methods and input data.

    Tip

    Distinguishing between HTTP methods can be done as follows:

    @app.route('/cars', methods=['GET', 'POST'])
    def cars():
      if request.method == 'GET':
        # Handle GET request
      elif request.method == 'POST':
        # Handle POST request
    

    (request is imported from flask) or by creating a separate method that only handles for example POST.

    Tip

    To extract the JSON data from the request you can for example use request.get_json(). The Content-Type header must be set to application/json. Here you can read more about this.

  2. Update the method for /cars/[int:car_id] so that it also supports updating via PUT. If a car with the requested id is not found you should respond with status code 404 (Not Found).

    For PUT you should take in a JSON object that represents a car or parts of a car and update the fields in the database that are specified in the incoming JSON object. The response should consist of an updated car.

    For example:

    REQUEST:
    PUT /cars/1
    
    {
        "make": "BMW"
    }
    
    RESPONSE:
    200 OK
    
    {
        "id": 1,
        "make": "BMW",
        "model": "A6"
    }
    

    and:

    REQUEST: PUT /cars/1
    
    { “id”: 1234, “make”: “Volvo”, “model”: “V70” }
    
    RESPONSE: 200 OK
    
    { "id”: 1, “make”: “Volvo”, “model”: “V70” }
    

    Note that the user should not be able to modify the id attribute.

  3. Implement functionality for DELETE /cars/[int:car_id] for deleting cars. If a car with the requested id is not found you should respond with status code 404 (Not Found).

    If the car is found it should be removed from the database and the response should only contain status code 200 (Success); no additional data is needed.

Part 4: Customer model and relations

Using the database and models, relations between different types of objects can be described. You will now extend the system with a customer model that has a relation to the car model. A car should be able to have 0 or 1 customer at a time, but a customer should be able to have 0 or more cars (One-to-Many relation, 1->N).

  1. Create a Customer model that contains at least the data id, name and email in the same way as the car model.

  2. Modify the car model and the customer model so that they have the relation described above (1->N). Note that a customer does not necessarily always need to have a relation to a car.

    Tip

    Read about relations in databases.

  3. For your changes to be reflected in the database you need to tell SQLAlchemy to apply the changes. The easiest way is simply to empty the database and recreate it. Open the Python interpreter and do the following:

    (venv) ~/tddd83-labs/server$ python
    Python 3.8.10 (default, Nov 26 2021, 20:14:08)
    [GCC 9.3.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>>
    
    >>> from main import db
    >>> db.drop_all()
    >>> db.create_all()
    

    Note that all data will be deleted! This approach is not recommended for anything other than simple testing/development. To make changes to databases where data cannot be lost, changes can for example be described in the form of migrations. See example 1 or example 2 if you want to find a suitable tool for this purpose.

  4. Now create, in the same way as before, functionality for

    • GET, POST /customers

    • GET, PUT, DELETE /customers/[int:customer_id]

  5. Update the functionality for GET /cars so that any customers are also shown in the response, i.e.:

    REQUEST:
    GET /cars
    
    RESPONSE:
    200 OK
    
    [
         {
             "id": 1,
             "make": "Volvo",
             "model": "V70",
             "customer": {
                 "id": 1,
                 "name": "Test Person",
                 "email": "test@test.t"
             }
         },
         {
             "id": 2,
             "make": "Audi",
             "model": "A8",
             "customer": null
         }
         ...
    ]
    

    Test this by creating a customer and then setting a relation to a car using the Python interpreter (see part 2 point 5 above) and letting a car be without a customer.

  6. Update the functionality for GET /cars/[int:car_id] so that any customer is also shown in the response. The response from your API should thus look like this if a car has a relation to a customer:

    _images/lab1-4-2.png
  7. Update the functionality for PUT /cars/[int:car_id] so that the attribute for the customer relation can also be updated, i.e.:

    REQUEST:
    PUT /cars/2
    
    {
        "customer_id": 1
    }
    

    The response from the server should look like for GET /cars/[int:car_id] above, with the new customer. If there is no customer with the specified id it is fine to leave the relation untouched and respond with status code 404.

  8. Add functionality for GET /customer/[int:customer_id]/cars. Your server should respond with a list of cars that have a relation to the customer in question, i.e.:

    [
        {
            "id": 1,
            "make": "Volvo",
            "model": "V70",
        },
        {
            "id": 2,
            "make": "Audi",
            "model": "A8",
        }
    ]
    

    If the customer has no relation to any car an empty list should be presented.

  9. Last but not least, update the functionality for deleting customers so that the relations are handled. If a customer is deleted any existing relations should also be nullified.

Demonstration

See Demonstration.