Flask REST API: The Easiest Way to Build RESTful Web Services

Flask REST API: The Easiest Way to Build RESTful Web Services
Photo by Andras Vas / Unsplash

Flask is a popular micro web framework for Python that makes it easy to build small web services, including REST APIs. REST APIs are web services that use the REST architectural style, which provides a standardized way of accessing and manipulating data over the web.

Here is an example of a Flask app that uses SQLAlchemy, an object-relational mapper (ORM), to store and retrieve customer data and exposes a REST API for managing customer records:

from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import or_

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'

db = SQLAlchemy(app)


class Customer(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)
    address = db.Column(db.String(120), nullable=False)


# We can force the db tables to be created using this snippet
with app.app_context():
    db.create_all()


@app.route('/customers', methods=['POST'])
def create_customer():
    data = request.get_json()
    new_customer = Customer(name=data['name'], address=data['address'])
    db.session.add(new_customer)
    db.session.commit()

    return jsonify({'id': new_customer.id}), 201


@app.route('/customers', methods=['GET'])
def read_customers():
    query = request.args.get('query')
    if query:
        # Query the database for customers that match the specified query
        customers = Customer.query.filter(or_(Customer.name.like(f"%{query}%"), Customer.address.like(f"%{query}%")))
    else:
        # Get all customers from the database
        customers = Customer.query.all()

    # Convert the customers to a list of dictionaries
    customers = [{'id': customer.id, 'name': customer.name, 'address': customer.address} for customer in customers]

    # Return the list of customers as JSON data
    return jsonify(customers)


@app.route('/customers/<int:customer_id>', methods=['GET'])
def read_customer(customer_id):
    customer = Customer.query.get(customer_id)

    if customer:
        return jsonify({'id': customer.id, 'name': customer.name, 'address': customer.address})
    else:
        return jsonify({'error': 'customer not found'})


@app.route('/customers/<int:customer_id>', methods=['PUT'])
def update_customer(customer_id):
    data = request.get_json()
    customer = Customer.query.get(customer_id)

    if customer:
        customer.name = data['name']
        customer.address = data['address']
        db.session.commit()

        return jsonify({'success': 'customer updated'})
    else:
        return jsonify({'error': 'customer not found'})


@app.route('/customers/<int:customer_id>', methods=['DELETE'])
def delete_customer(customer_id):
    customer = Customer.query.get(customer_id)

    if customer:
        db.session.delete(customer)
        db.session.commit()

        return jsonify({'success': 'customer deleted'})
    else:
        return jsonify({'error': 'customer not found'})


if __name__ == '__main__':
    app.run()

This Flask app defines several functions and routes that expose a REST API for managing customer records:

  • Connecting to the SQLite database before each request using the before_request method.
  • Closing the database connection after each request using the teardown_request method.
  • Defining the GET /customers route, which returns a list of customers from the customers table in the database. If a query parameter is provided in the URL, the app will filter the results based on the query and only return customers that match the query.
  • Defining the POST /customers route, which accepts a POST request and adds a new customer to the customers table in the database using the data from the request form.
  • Defining the PUT /customers/ route, which accepts a PUT request and updates the customer with the specified id in the customers table in the database using the data from the request form.
  • Defining the DELETE /customers/ route, which accepts a DELETE request and deletes the customer with the specified id from the customers table in the database.

In addition to the basic CRUD operations, this Flask app could also include other features to make it more useful and user-friendly. For example, you could add pagination to the GET /customers route to allow clients to specify the number of customers to return per page and the page number. You could also add sorting to the GET /customers route to allow clients to sort the customer list by different criteria, such as name, email, or date added.

Another useful feature that you could add to this Flask app is support for filtering customers based on different criteria. For example, you could add a GET /customers?email= route that allows clients to filter the customer list by email address. You could also add support for other query parameters, such as name, date_added, and status, to allow clients to filter the customer list in various ways.

Lets add some tests!

from flask_testing import TestCase
from app import app, Customer, db


class FlaskAppTests(TestCase):
    SQLALCHEMY_DATABASE_URI = "sqlite://"
    TESTING = True

    def create_app(self):
        # create a Flask application context
        # create a test database in memory and add some test data
        # create a test client
        self.app = app.test_client()
        # propagate the exceptions to the test client
        self.app.testing = True

        return app

    def setUp(self):
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    def test_create_customer(self):
        # send a POST request to the '/customers' endpoint with the data for the new customer
        result = self.client.post('/customers', json={'name': 'John Doe', 'address': '123 Main St'})

        # assert that the response status code is 201
        self.assertEqual(result.status_code, 201)

        # convert the response data from JSON to a dictionary
        data = result.get_json()
        # assert that the dictionary contains a 'id' key
        self.assertIn('id', data)

        # get the customer with the id from the response data
        customer = Customer.query.get(data['id'])
        # assert that the customer is not None
        self.assertIsNotNone(customer)

    def test_read_customers(self):
        # create some customers to query
        customer1 = Customer(name='Jane Doe', address='456 Park Ave')
        customer2 = Customer(name='John Smith', address='789 Elm St')
        db.session.add(customer1)
        db.session.add(customer2)
        db.session.commit()

        # send a GET request to the '/customers' endpoint
        result = self.client.get('/customers')

        # assert that the response status code is 200
        self.assertEqual(result.status_code, 200)

        # convert the response data from JSON to a list of dictionaries
        data = result.get_json()
        # assert that the list contains two dictionaries
        self.assertEqual(len(data), 2)

        # assert that the first dictionary contains the correct customer data
        self.assertEqual(data[0]['name'], 'Jane Doe')
        self.assertEqual(data[0]['address'], '456 Park Ave')
        # assert that the second dictionary contains the correct customer data
        self.assertEqual(data[1]['name'], 'John Smith')
        self.assertEqual(data[1]['address'], '789 Elm St')

    def test_read_customers_with_query(self):
        # create some customers to query
        customer1 = Customer(name='Jane Doe', address='456 Park Ave')
        customer2 = Customer(name='John Smith', address='789 Elm St')
        customer3 = Customer(name='Bob Johnson', address='321 Pine St')
        db.session.add(customer1)
        db.session.add(customer2)
        db.session.add(customer3)
        db.session.commit()

        # send a GET request to the '/customers' endpoint with a query parameter
        result = self.client.get('/customers?query=Smith')

        # assert that the response status code is 200
        self.assertEqual(result.status_code, 200)

    def test_read_customer(self):
        customer1 = Customer(name='Jane Doe', address='456 Park Ave')
        customer2 = Customer(name='John Smith', address='789 Elm St')
        db.session.add(customer1)
        db.session.add(customer2)
        db.session.commit()
        # send a GET request to the '/customers/<customer_id>' endpoint
        result = self.client.get('/customers/1')

        # assert that the response status code is 200
        self.assertEqual(result.status_code, 200)

        # convert the response data from JSON to a dictionary
        data = result.get_json()
        # assert that the dictionary contains the correct customer data
        self.assertEqual(data['name'], 'Jane Doe')
        self.assertEqual(data['address'], '456 Park Ave')

        # send a GET request to the '/customers/<customer_id>' endpoint with an invalid customer id
        result = self.client.get('/customers/999')

        # assert that the response status code is 200
        self.assertEqual(result.status_code, 200)

        # convert the response data from JSON to a dictionary
        data = result.get_json()
        # assert that the dictionary contains an 'error' key
        self.assertIn('error', data)

    def test_update_customer(self):
        customer1 = Customer(name='Jane Doe', address='456 Park Ave')
        customer2 = Customer(name='John Smith', address='789 Elm St')
        db.session.add(customer1)
        db.session.add(customer2)
        db.session.commit()
        # send a PUT request to the '/customers/<customer_id>' endpoint with updated data for the customer
        result = self.client.put('/customers/1', json={'name': 'Jane Smith', 'address': '456 Park Ave'})

        # assert that the response status code is 200
        self.assertEqual(result.status_code, 200)

        # convert the response data from JSON to a dictionary
        data = result.get_json()
        # assert that the dictionary contains a 'success' key
        self.assertIn('success', data)

        # get the updated customer from the database
        customer = Customer.query.get(1)
        # assert that the customer's data was updated correctly
        self.assertEqual(customer.name, 'Jane Smith')
        self.assertEqual(customer.address, '456 Park Ave')

        # send a PUT request to the '/customers/<customer_id>' endpoint with an invalid customer id
        result = self.client.put('/customers/999', json={'name': 'Jane Smith', 'address': '456 Park Ave'})

        # assert that the response status code is 200
        self.assertEqual(result.status_code, 200)

        # convert the response data from JSON to a dictionary
        data = result.get_json()
        # assert that the dictionary contains an 'error' key
        self.assertIn('error', data)
  • test_create_customer() tests that the app can create a new customer by sending a POST request to the '/customers' endpoint with the data for the new customer. It asserts that the response has a status code of 201 and that the response data includes an 'id' key for the new customer and that the customer is saved in the database.
  • test_read_customers() tests that the app can return a list of all customers by sending a GET request to the '/customers' endpoint. It creates some test customers in the database, sends the request, and asserts that the response has a status code of 200 and that the response data is a list of dictionaries containing the correct customer data.
  • test_read_customers_with_query() tests that the app can return a list of customers matching a query by sending a GET request to the '/customers' endpoint with a query parameter. It creates some test customers in the database, sends the request with a query parameter, and asserts that the response has a status code of 200.
  • test_read_customer() tests that the app can return a specific customer by sending a GET request to the '/customers/<customer_id>' endpoint. It creates some test customers in the database, sends the request with a customer id, and asserts that the response has a status code of 200 and that the response data is a dictionary containing the correct customer data.
  • test_update_customer() tests that the app can update a specific customer by sending a PUT request to the '/customers/<customer_id>' endpoint with the updated data for the customer. It creates a test customer in the database, sends the request with the updated data, and asserts that the response has a status code of 200 and that the customer in the database has been updated with the new data.
  • test_delete_customer() tests that the app can delete a specific customer by sending a DELETE request to the '/customers/<customer_id>' endpoint. It creates a test customer in the database, sends the request with the customer id, and asserts that the response has a status code of 200 and that the customer has been removed from the database.

In addition to these features, you could also improve the user experience of the Flask app by implementing a more intuitive and user-friendly design. This could include adding a navigation bar, search bar, and other UI elements to make the app easier to use and navigate.

Overall, Flask is a powerful and flexible framework for building REST APIs, and with the right features and design, you can create a user-friendly and useful API for storing and managing customer data. To learn more about Flask, check out the official Flask documentation.

In addition to its minimalist design and scalability, Flask is also known for its simplicity and flexibility compared to other web frameworks. Unlike some other web frameworks, Flask does not force you to follow a specific directory structure or use certain libraries and tools. Instead, Flask provides a small core of features and lets you choose the libraries and tools that you want to use with your app.

This flexibility and simplicity make Flask a great choice for building small web services that have specific requirements and constraints. For example, if you are building a small web service that only needs to handle a few routes and does not require a lot of features, you can use Flask without adding any additional libraries or components. This allows you to keep your app lightweight and easy to maintain.

However, if you are building a larger and more complex web service, Flask may not provide as much out-of-the-box functionality as some other web frameworks. For example, some other web frameworks, such as Django and Ruby on Rails, provide a wide range of features and tools for building web applications, such as ORMs, templates, forms, and authentication. These frameworks may be a better choice for building larger and more complex web services that require more features and functionality.

Overall, Flask is a simple and flexible framework that is well-suited for building small web services. While it may not provide as many out-of-the-box features as some other web frameworks, Flask's simplicity and flexibility make it a great choice for building web services that have specific requirements and constraints. To learn more about Flask and its comparisons to other web frameworks, check out the official Flask documentation.