In this comprehensive guide, we will explore the process of building a RESTful API in Python using the Flask framework. Whether you’re a beginner or an experienced developer, this step-by-step tutorial will walk you through the entire process, from setting up Flask to implementing CRUD (Create, Read, Update, Delete) functionality.

But wait, there’s more! I’ll provide you with an API collection that you can import into Postman for easy testing. This collection includes all the CRUD request details like endpoints, methods, and request bodies. And if you prefer a visual approach, I’ve got you covered. I’ll dedicate a section to guiding you through setting up a frontend application that can seamlessly interact with the Flask API, without requiring any frontend code.

Flask, being a micro-framework, focuses on simplicity and provides only the essential features needed for web application development. It’s an excellent choice for small to medium-sized projects with straightforward requirements. However, if you’re working on a complex web application, I highly recommend Django. Django is a full-featured framework that comes with batteries already included, providing extensive capabilities out of the box.

With that said, let’s dive into the world of Flask and start building our RESTful API!

More practice:

Use Flask Framework to Build a RESTful API in Python

Running the Flask Project on your Computer

  1. Download or clone the Flask project from the GitHub repository: https://github.com/wpcodevo/flask-note-taking-api. Open the source code in your preferred code editor.
  2. Create a virtual environment for the Flask project by running the command python3 -m venv venv. This will set up a dedicated environment for the project.
  3. Once the virtual environment is created, activate it. If you’re using a Unix-based system, run the command source venv/bin/activate. For Windows users, run venv\Scripts\activate.
  4. Install the required dependencies for the Flask app by running the command pip install -r requirements.txt in the terminal of the root directory. This will install all the necessary packages listed in the requirements.txt file.
  5. Launch a PostgreSQL server in a Docker container by running the command docker-compose up -d. This will start the database server in the background.
  6. After starting the PostgreSQL server, apply the SQLAlchemy migrations to the database by running the command flask db upgrade. This will create the necessary tables and schema in the database.
  7. Start the Flask development server with the command flask run. The Flask app will be accessible at http://localhost:8000.
  8. Now that the Flask app is running, it’s time to test the endpoints. To simplify your life, I’ve included the API collection I used to test the Flask app in the source code. You can access it by importing the Note App.postman_collection.json file into either Postman or the Thunder Client VS Code extension.

Running the Flask App with a Frontend Application

Assuming you prefer using a frontend app to test the Flask application, I’ve got you covered. You can follow the steps below to set up a frontend app built with React.js that allows you to interact with the endpoints on the Flask app without writing a single line of React code. If you need a detailed guide on building the frontend application, refer to the article “Build a React.js CRUD App with JavaScript Fetch API“.

  1. Download or clone the React.js project from the repository: https://github.com/wpcodevo/react-crud-fetchapi-app. Open the project in your preferred IDE or text editor.
  2. In the terminal of the project’s root directory, run yarn or yarn install to install all the project’s dependencies.
  3. Start the app on port 3000 by running yarn dev.
  4. Open a new tab in your browser and enter http://localhost:3000/ to access the React app. Note that opening the app on http://127.0.0.1:3000 may result in a “site can’t be reached” or CORS errors.

Setting up the Flask Project

In this section, we’ll prepare the development environment for your Flask project and install the required dependencies. To ensure that everything is set up correctly, we’ll start by creating a basic Flask server with a single endpoint called the health checker. This endpoint will allow us to make requests and verify the server’s health. By doing this, we can confirm that the initial configurations are correct before proceeding to add more functionality.

Before we begin, it’s important to ensure that you have Python 3 installed on your machine. If you’re using a recent version of a popular Linux/Unix operating system or macOS, Python 3 may already be installed by default. However, if you’re using Windows, you’ll likely need to install Python 3 manually, as it’s not included with the operating system.

To check if Python 3 is installed, you can run the following commands based on your operating system:


# For Unix or macOS
python3 --version 

# For Windows
python --version

Upon executing the command, the console will display the Python version installed on your system. Please note that the exact version may vary depending on the specific binary you installed. However, what matters is that you have at least Python 3.7 or a newer version installed.

Project Structure

Below is a screenshot illustrating the project structure. It serves as a visual guide to help you navigate the folder hierarchy and ensure you’re on the right path. If you encounter any challenges during the tutorial, you can refer back to this image for guidance and clarity.

Folder and File Structure of the Flask and SQLAlchemy Project of the Note Taking Application

Setting Up the Flask Development Environment

Let’s get started with configuring the development environment for your Flask application. Follow these steps to proceed:

1. Create a Directory for Your Flask Project

Choose a location where you typically store your Python projects. If you don’t have a specific location in mind, you can create the project directly on your Desktop. Open a new terminal and execute the following commands:


mkdir flask-note-taking-api
cd flask-note-taking-api 

Feel free to choose any name for the directory.

2. Set up a virtual environment for the project

Create a virtual environment for your project to keep its dependencies separate from your system-level Python installation. Execute the following command in the terminal:


# For Linux/Unix or macOS
python3 -m venv venv

# For Windows
python -m venv venv

Open the project in your preferred code editor.

3. Activate the virtual environment

In the integrated terminal of your code editor, activate the virtual environment using the appropriate command:


# For Linux/Unix or macOS
source venv/bin/activate

# For Windows
.\venv\Scripts\activate

To confirm that the virtual environment is activated, you can run the command env | grep VIRTUAL_ENV.

4. Install Flask and Dotenv

Now, let’s install Flask and the python-dotenv module by executing the following command:


pip install flask python-dotenv

  • flask – This module provides a simple and flexible way to handle routing, request handling, and rendering templates.
  • python-dotenv – This module allows you to load variables from a configuration file into your application’s environment.

Building a Basic Flask Application

Now that you have created the project folder, set up the virtual environment, and installed the necessary dependencies, we can proceed to set up a basic Flask server to get started. In this tutorial, we will be using the factory pattern, which is the recommended approach for creating Flask applications. This pattern allows you to create multiple instances of the Flask application, such as for testing or deployment in different environments.

To begin, create a ‘src‘ directory in the root level of your project. Inside the src directory, create an __init__.py file and add the following code:

src/__init__.py


from flask import Flask


def create_app():
    app = Flask(__name__, instance_relative_config=True)

    @app.get('/api/healthchecker')
    def healthchecker():
        return {"status": "success", "message": "Build RESTful API with Flask and SQLAlchemy"}

    return app

This code sets up a Flask application using the factory pattern. The create_app() function creates an instance of the Flask application and defines a single endpoint /api/healthchecker that returns a JSON response with a success message.

To start the Flask development server, we need to inform Flask about the entry point of our Flask application. This ensures that Flask can locate and run the application when the server is started. There are a couple of ways to achieve this.

One approach is to manually set the FLASK_APP environment variable to the src directory by executing the command export FLASK_APP=src. This command sets the FLASK_APP environment variable to the desired directory. Afterwards, you can run the command flask run to start the server. While this method works, you would need to repeat the process if you open a new terminal.

To simplify this process, we can automate it by creating a special file named .flaskenv in the root directory. This file will contain the necessary Flask configurations, and Flask will execute the commands in this file automatically before starting the server. To proceed with this approach, follow these steps:

  1. Create a file named .flaskenv in the root directory.
  2. Add the following code to the .flaskenv file:

.flaskenv


export FLASK_ENV=development
export FLASK_APP=src
export FLASK_RUN_PORT=8000
export FLASK_RUN_HOST=0.0.0.0

Now, let’s start the Flask development server. Open your terminal and execute the command flask run --reload. This command not only launches the Flask server but also enables automatic reloading whenever changes are made to the source code. This means that you don’t need to manually restart the server after modifying any files.

Once the server is up and running, you can access the health checker endpoint by visiting http://localhost:8000/api/healthchecker in your browser. This URL will send a GET request to the Flask server. Within a few milliseconds, you should receive a JSON response containing the information we returned from the health checker route.

Access the Health Checker Route of the Flask Server

Setting Up a PostgreSQL Database

In this section, we’ll use Docker to set up PostgreSQL and pgAdmin servers. Instead of manually installing the PostgreSQL and pgAdmin binaries, Docker allows us to run both servers with a single command. You might be wondering why we need both the PostgreSQL and pgAdmin servers. Well, pgAdmin is a handy graphical user interface (GUI) tool that lets us easily manage and interact with our PostgreSQL database.

If you don’t have Docker installed, follow the installation guide for your operating system at https://docs.docker.com/get-docker/.

Configuring Docker for PostgreSQL

When it comes to running Docker containers on our machine, there are multiple options available. However, for the sake of simplicity, we will utilize Docker Compose. This tool allows us to define the required configurations for containers and effortlessly start or stop them with a single command.

To get started, create a docker-compose.yml file in the root directory and include the following YAML configurations:

docker-compose.yml


version: "3"
services:
  postgres:
    image: postgres:latest
    container_name: postgres
    ports:
      - "6500:5432"
    volumes:
      - progresDB:/var/lib/postgresql/data
    env_file:
      - ./.env
  pgAdmin:
    image: dpage/pgadmin4
    container_name: pgAdmin
    env_file:
      - ./.env
    ports:
      - "5050:80"
volumes:
  progresDB:

The above configurations will pull the latest images of Postgres and pgAdmin from DockerHub if they do not already exist on your machine. It will then proceed to build the images using the credentials specified in a .env file. Finally, it will run the images in their respective Docker containers.

Running the PostgreSQL and pgAdmin Servers

Now it’s time to run the containers, but before that, we need to address one step. In the Docker Compose file, you’ll notice that a .env file is specified to provide the credentials required by the Postgres and pgAdmin images. Therefore, we need to create this file and make the credentials available. Follow these steps:

  1. Create a file named .env in the root level of your project.
  2. Add the following variables to the .env file:

.env


POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=6500
POSTGRES_USER=admin
POSTGRES_PASSWORD=password123
POSTGRES_DB=flask_db

PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=password123

With the credentials in place, you can run the command docker-compose up -d in your terminal to launch the PostgreSQL and pgAdmin servers. This command will start the containers in the background, allowing you to continue working without being attached to their logs. Once the setup is complete, you can verify if the containers are running by using the Docker Desktop application or running the command docker ps in your terminal.

View the Running Containers on Docker Desktop

Creating the Database Model

At this stage, we have successfully set up a running PostgreSQL server. Now, the next step is to establish a connection between the Flask application and the running PostgreSQL server. To achieve this, we will be utilizing the SQLAlchemy ORM, which requires some initial configuration to get it up and running smoothly.

Fortunately, there is a Flask plugin called flask_sqlalchemy that simplifies the process by acting as a wrapper and seamlessly integrating with Flask. It’s important to note that SQLAlchemy relies on a Postgres driver to facilitate communication with the Postgres server. Therefore, you will also need to install the psycopg2-binary module separately.

To install both flask_sqlalchemy and psycopg2-binary, execute the following commands in your terminal:


pip install Flask-SQLAlchemy
pip install psycopg2-binary

With these dependencies installed, we can now start modelling the data required for the Flask application using SQLAlchemy. Since we are building a note-taking API, there are various models to consider, including User, Note, Tag, and more. However, for the sake of simplicity, we’ll focus on the Note model to enable CRUD operations. To proceed, create a models.py file in the src directory and add the following code to it:

src/models.py


from datetime import datetime
import uuid
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Note(db.Model):
    __tablename__ = 'notes'

    id = db.Column(db.String(36), primary_key=True, default=str(uuid.uuid4()))
    title = db.Column(db.String(100), unique=True)
    content = db.Column(db.Text)
    category = db.Column(db.String(100), nullable=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(
        db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    published = db.Column(db.Boolean, default=False, nullable=True)

    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'content': self.content,
            'category': self.category,
            'createdAt': self.created_at.isoformat(),
            'updatedAt': self.updated_at.isoformat(),
            'published': self.published
        }

In the code snippet above, we imported the SQLAlchemy class and created an instance of it. Using this instance, we defined the Note model by specifying its fields. We also included a to_dict method, which helps us convert the data retrieved from SQLAlchemy into a dictionary format. This makes it easier for us to work with the data in our application.

Database Migration

Now that we have defined the SQLAlchemy model, it’s important to generate migration files for it. These files allow us to keep track of any changes made to the database schema and provide the ability to roll back to previous versions if necessary.

Setting up Flask-Migrate

Normally, Alembic is used alongside SQLAlchemy to perform database migrations in Python projects. However, when working with Flask, we have the advantage of a convenient extension called Flask-Migrate that handles SQLAlchemy database migrations for us with ease.

It’s important to note that the Flask-Migrate extension utilizes Alembic behind the scenes, abstracting the setup process that would otherwise be required if using Alembic directly. To install the Flask-Migrate extension, simply run the command provided below in your terminal:


pip install Flask-Migrate

Once you have installed the Flask-Migrate extension, the next step is to instantiate it in your project. It is considered a best practice to perform this instantiation within the same module where you have defined your SQLAlchemy models. In our scenario, the models are defined in the src/models.py file. To accomplish this, open the src/models.py file and replace the existing code with the following updated code snippet:

src/models.py


from datetime import datetime
import uuid
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()


class Note(db.Model):
    __tablename__ = 'notes'

    id = db.Column(db.String(36), primary_key=True, default=str(uuid.uuid4()))
    title = db.Column(db.String(100), unique=True)
    content = db.Column(db.Text)
    category = db.Column(db.String(100), nullable=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(
        db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    published = db.Column(db.Boolean, default=False, nullable=True)

    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'content': self.content,
            'category': self.category,
            'createdAt': self.created_at.isoformat(),
            'updatedAt': self.updated_at.isoformat(),
            'published': self.published
        }

Generating and Applying Migrations

Before we can generate the migration files, we need to initialize the SQLAlchemy and Flask-Migrate extensions with our Flask application. To do this, we’ll perform the initializations in the src/__init__.py file, which serves as the entry point of our project.

src/__init__.py


import os
from flask import Flask
from dotenv import load_dotenv
from src.models import db, migrate


def create_app():
    app = Flask(__name__, instance_relative_config=True)

    load_dotenv()  # Load environment variables from .env file
    database_uri = os.getenv('SQLALCHEMY_DATABASE_URI')
    app.config['SQLALCHEMY_DATABASE_URI'] = database_uri

    db.init_app(app)
    migrate.init_app(app, db)

    @app.get('/api/healthchecker')
    def healthchecker():
        return {"status": "success", "message": "Build RESTful API with Flask and SQLAlchemy"}

    return app

In the provided code, we started by loading the environment variables from the .env file into the Python environment using the load_dotenv() method. This enables us to access these variables within the Flask application using the os.getenv() method.

Next, we obtained the SQLAlchemy database connection URL and added it to the application’s configuration object. This URL is crucial for SQLAlchemy to establish a connection with the PostgreSQL server and perform necessary database operations.

To complete the setup, we initialized both SQLAlchemy and Flask-Migrate extensions:

  1. db.init_app(app): This line initializes the SQLAlchemy extension with the Flask application, allowing us to interact with the database using SQLAlchemy.
  2. migrate.init_app(app, db): This line initializes the Flask-Migrate extension with the Flask application and SQLAlchemy database. Flask-Migrate handles database migrations, keeping track of schema changes and allowing us to easily update the database structure when needed.

Before running the migration commands, it’s important to include the SQLALCHEMY_DATABASE_URI variable in the .env file. This variable should contain the connection URL to the PostgreSQL database that is currently running. Without setting this variable, the migration commands will not work properly. To address this, open the .env file and update its contents by adding the SQLALCHEMY_DATABASE_URI variable.

.env


POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=6500
POSTGRES_USER=admin
POSTGRES_PASSWORD=password123
POSTGRES_DB=flask_db

PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=password123

SQLALCHEMY_DATABASE_URI=postgresql://admin:password123@localhost:6500/flask_db

Ensure that the PostgreSQL Docker container is running, and then use the following commands to generate migration files and synchronize the database schema:


flask db init
flask db migrate -m "Initial migration"
flask db upgrade

  1. flask db init: This command initializes the database migration environment. It creates a migrations directory in your project, which will store the migration scripts.
  2. flask db migrate -m "Initial migration": This command generates an initial migration script based on the changes detected in the models. The -m option is used to provide a descriptive message for the migration. It creates a new migration file in the migrations/versions directory.
  3. flask db upgrade: This command applies the database migrations to the database. It executes the migration scripts that have not been applied yet. It updates the database schema according to the changes specified in the migration files.

To log in to the PostgreSQL server from pgAdmin and verify if the commands executed previously created the notes table, follow these steps:

  1. Access the pgAdmin application running in the Docker container by visiting http://localhost:5050/ in your web browser.
  2. Sign in to pgAdmin using the credentials specified in the .env file.
  3. Once logged in, click on “Add New Server” to open a popup.
  4. In the popup, provide the necessary credentials for the PostgreSQL server running in the Docker container. Retrieve the password from the .env file.
  5. To obtain the “Host name/address“, run the command docker inspect postgres in your terminal. Scroll down to the bottom and copy the IP address specified in the “IPAddress” field.
  6. Paste the copied IP address into the “Host name/address” field in the popup.
Flask App Add a New Postgres Server on pgAdmin

Once you have added the Postgres server to pgAdmin, navigate to the “Schemas” section and then to the “Tables” section within the flask_db database. You will find two tables: alembic_version and the ‘notes‘ table. The alembic_version table is specifically used by Alembic to keep track of the current migration version of the database. It helps Alembic manage and organize the database migrations effectively.

Flask App Check the Notes Table in the Postgres Database

Creating the Request Validation Schemas

In API development, it is considered a best practice to validate incoming request bodies before they reach our route handlers. This helps filter out irrelevant or malicious data, preventing potential crashes in our application. Additionally, request body validation ensures that users provide the correct data types and adhere to the required validation rules. To define validation schemas, we will utilize the widely-used WTForms library.

To seamlessly integrate WTForms with our Flask app, we also need to install the Flask-WTF extension. Execute the command below to install both dependencies:


pip install WTForms Flask-WTF

  • WTForms is a Python library that offers form validation and rendering capabilities. It simplifies the process of defining, validating, and rendering complex forms by providing reusable components.
  • Flask-WTF – is an extension specifically designed for Flask applications. It seamlessly integrates the features of WTForms into Flask, allowing you to easily handle form validation and rendering within your Flask projects.

To define the validation schemas using the installed libraries, follow these steps:

  1. Create a new file named schemas.py inside the src directory.
  2. Open the schemas.py file and add the following code:

src/schemas.py


from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, BooleanField
from wtforms.validators import DataRequired, Length, Optional


class CreateNoteForm(FlaskForm):
    title = StringField('Title', validators=[DataRequired(), Length(max=100)])
    content = TextAreaField('Content', validators=[DataRequired()])
    category = TextAreaField('Category', validators=[Optional()])
    published = BooleanField('Published', validators=[Optional()])


class UpdateNoteForm(FlaskForm):
    title = StringField('Title', validators=[Optional(), Length(max=100)])
    content = TextAreaField('Content', validators=[Optional()])
    category = TextAreaField('Category', validators=[Optional()])
    published = BooleanField('Published', validators=[Optional()])

In the above code, we import the necessary classes and functions from the flask_wtf and wtforms modules. We define two classes: CreateNoteForm and UpdateNoteForm, which inherit from FlaskForm. Each class represents a form and includes fields with their respective validators.

For the CreateNoteForm, the title and content fields are required and have a maximum length of 100 characters. The ‘category‘ and published fields are optional.

Similarly, for the UpdateNoteForm, all fields are optional. This allows for partial updates of the note data.

CRUD Route Handlers

Now it’s time to use SQLAlchemy to perform CRUD operations on the PostgreSQL database. To interact with the database, we will create the following route handlers in a notes_routes.py file within the src directory. These route handlers will handle different types of requests and enable us to manipulate the data:

  1. create_note: This route handler will handle POST requests to the /api/notes endpoint. It will add new notes to the database.
  2. update_note: This route handler will handle PATCH requests to the /api/notes/<string:note_id> endpoint. It will edit an existing note in the database.
  3. get_note: This route handler will handle GET requests to the /api/notes/<string:note_id> endpoint. It will retrieve a single note from the database.
  4. delete_note: This route handler will handle DELETE requests to the /api/notes/<string:note_id> endpoint. It will delete a note from the database.
  5. get_notes: – This route handler will handle GET requests to the /api/notes endpoint. It will fetch a paginated list of notes from the database.

The Flask-WTF extension requires a CSRF token to be included in the request headers and body. This typically involves defining a separate server route to obtain the CSRF token. In our case, due to the small size of the project, we will omit this step and exempt the handlers from using CSRF protection. However, in a larger and more complex project, it is highly recommended to implement the necessary code for CSRF protection.

To implement this, create a new file named utils.py in the ‘src’ directory. Inside utils.py, add the following code snippet:

src/utils.py


from flask_wtf import CSRFProtect

csrf = CSRFProtect()

Next, create a notes_routes.py file in the src directory and include the following import statements.

src/notes_routes.py


from src.models import db, Note
from src.schemas import CreateNoteForm, UpdateNoteForm
from flask import request
from datetime import datetime
from flask_wtf.csrf import generate_csrf
from sqlalchemy.exc import IntegrityError
import src.utils as utils

Creating a Resource

Let’s take a look at the ‘create_note‘ function, which is responsible for handling the POST requests to the /api/notes endpoint in the Flask app.

Upon invocation, the function begins by initializing an instance of the CreateNoteForm and using it to validate the data provided in the request body. If the form validation fails, a response with a status code of 400 (Bad Request) is returned, including the validation errors obtained from the form.

To ensure CSRF (Cross-Site Request Forgery) protection, the function generates a CSRF token using the generate_csrf function provided by the Flask-WTF library. This token is then appended to the request body as the value of the csrf_token field. This mechanism ensures that the form submission includes a valid CSRF token for successful validation.

src/notes_routes.py


@utils.csrf.exempt
def create_note():
    body = CreateNoteForm()

    csrf_token = generate_csrf()
    body.csrf_token.data = csrf_token

    if body.validate_on_submit():
        try:
            note = Note(
                title=body.title.data,
                content=body.content.data,
            )
            if body.category.data:
                note.category = body.category.data

            db.session.add(note)
            db.session.commit()
            return {'status': 'success', 'data': {'note': note.to_dict()}}, 201
        except IntegrityError:
            db.session.rollback()
            return {'status': 'fail', 'message': 'Note with the same title already exists'}, 409

    else:
        return {'status': 'fail', 'errors': body.errors}, 400

Moving forward, the function adds the newly created Note object to the session and commits it to the database. In the event of an IntegrityError, indicating a conflict with an existing note title, a rollback operation is performed on the session. A response with a status code of 409 (Conflict) is returned, along with an appropriate error message.

On the other hand, if the note creation is successful, the function returns a JSON response with a status code of 201 (Created). The response body includes the data of the created note, allowing the client to access the newly created resource.

Updating a Resource

Here, you will create the update_note function, which handles PATCH requests to the /api/notes/<string:note_id> endpoint.

When this function is called, it begins by initializing an instance of the UpdateNoteForm. This form is then used to validate the data provided in the request body. To ensure successful form validation, the function generates a CSRF token and assigns it to the csrf_token field of the form.

Next, the function retrieves the note object from the database using the provided note_id. If the note exists, it proceeds to update the note’s attributes based on the data provided in the request body. Each attribute is individually checked, and if a corresponding value is present in the request body, the note’s attribute is updated accordingly. The changes are then committed to the database by invoking db.session.commit().

src/notes_routes.py


@utils.csrf.exempt
def update_note(note_id):
    body = UpdateNoteForm()

    csrf_token = generate_csrf()
    body.csrf_token.data = csrf_token

    if body.validate_on_submit():
        note = Note.query.get(note_id)

        if note:
            if body.title.data:
                note.title = body.title.data
            if body.content.data:
                note.content = body.content.data
            if body.category.data:
                note.category = body.category.data
            if body.published.data:
                note.published = body.published.data

            note.updated_at = datetime.utcnow()
            db.session.commit()

            return {'status': 'success', 'data': {'note': note.to_dict()}}
        else:
            return {"status": "fail", 'message': f'Note with id {note_id} not found'}, 404
    else:
        return {'status': 'fail', 'errors': body.errors}, 400

In the case of a successful note update, the function returns a JSON response with a status code of 200 (OK). The response body includes the updated note’s data in the note field.

However, if the note with the specified note_id is not found in the database, the function returns a response with a status code of 404 (Not Found).

Retrieving a Resource

Now let’s explore the get_note function, which is responsible for querying a single note from the database and returning the corresponding record. When this function is called, it performs a query to the database to fetch a note based on the provided note_id from the request.

If the note exists in the database, the function responds with a JSON object containing a status of “success” and includes the note’s data in the response body.

src/notes_routes.py


def get_note(note_id):
    note = Note.query.get(note_id)

    if note:
        return {"status": "success", 'data': {'note': note.to_dict()}}
    else:
        return {"status": "fail", 'message': f'Note with id {note_id} not found'}, 404

Deleting a Resource

Let’s now tackle the delete_note function. This function is responsible for deleting a note from the database based on the provided note_id.

When the function is called, it retrieves the note object from the database using the ‘note_id‘ parameter. If the note exists, it proceeds to delete the note by using the delete() method from the SQLAlchemy session. After deleting the note, the changes are committed to the database by calling db.session.commit().

If the note is successfully deleted, the function returns a JSON response with a status of “success” and a message indicating that the note was deleted successfully.

src/notes_routes.py


@utils.csrf.exempt
def delete_note(note_id):
    note = Note.query.get(note_id)

    if note:
        db.session.delete(note)
        db.session.commit()
        return {'status': 'success', 'message': 'Note deleted successfully'}
    else:
        return {"status": "fail", 'message': f'Note with id {note_id} not found'}, 404

Retrieving all Resources

In the subsection titled ‘Retrieving a Resource‘, we have completed the implementation of the first GET request, which allows us to retrieve a single note from the database. Now, let’s proceed with implementing another GET request that will enable us to fetch a list of note items. This new function, get_notes, incorporates pagination functionality to efficiently handle large datasets by limiting the number of results returned to the client.

Upon invocation, the function retrieves the values of the page and limit parameters from the request’s query parameters. If these parameters are not provided, default values of 1 for page and 10 for limit are used.

Next, the function executes a query to retrieve the notes from the database using the paginate method from the SQLAlchemy query object. The paginate method handles the pagination logic, including the specified page number and the number of notes per page.

The notes are then extracted from the paginated object by accessing the items attribute. For each note, the to_dict() method is called to convert it into a dictionary representation.

src/notes_routes.py


def get_notes():
    page = request.args.get('page', default=1, type=int)
    per_page = request.args.get('limit', default=10, type=int)

    notes = Note.query.paginate(page=page, per_page=per_page, error_out=False)

    note_list = [note.to_dict() for note in notes.items]
    result = {
        'status': 'success',
        'notes': note_list,
        'page': notes.pages,
        'limit': notes.per_page,
        'results': len(note_list)
    }

    return result

To provide additional context to the client, the function constructs a result object that encompasses the retrieved notes, along with supplementary metadata such as the total number of pages (pages attribute), the number of notes per page (per_page attribute), and the total number of results (len(note_list)).

Finally, the function returns the result object, indicating a successful operation with a status of “success”.

The Complete Code of the Route Handlers

src/notes_routes.py


from src.models import db, Note
from src.schemas import CreateNoteForm, UpdateNoteForm
from flask import request
from datetime import datetime
from flask_wtf.csrf import generate_csrf
from sqlalchemy.exc import IntegrityError
import src.utils as utils

@utils.csrf.exempt
def create_note():
    body = CreateNoteForm()

    csrf_token = generate_csrf()
    body.csrf_token.data = csrf_token

    if body.validate_on_submit():
        try:
            note = Note(
                title=body.title.data,
                content=body.content.data,
            )
            if body.category.data:
                note.category = body.category.data
            
            db.session.add(note)
            db.session.commit()
            return {'status': 'success', 'data': {'note': note.to_dict()}}, 201
        except IntegrityError:
            db.session.rollback()
            return {'status': 'fail', 'message': 'Note with the same title already exists'}, 409

    else:
        return {'status': 'fail', 'errors': body.errors}, 400

@utils.csrf.exempt
def update_note(note_id):
    body = UpdateNoteForm()

    csrf_token = generate_csrf()
    body.csrf_token.data = csrf_token

    if body.validate_on_submit():
        note = Note.query.get(note_id)

        if note:
            if body.title.data:
                note.title = body.title.data
            if body.content.data:
                note.content = body.content.data
            if body.category.data:
                note.category = body.category.data
            if body.published.data:
                note.published = body.published.data

            note.updated_at = datetime.utcnow()
            db.session.commit()

            return {'status': 'success', 'data': {'note': note.to_dict()}}
        else:
            return {"status": "fail", 'message': f'Note with id {note_id} not found'}, 404
    else:
        return {'status': 'fail', 'errors': body.errors}, 400


def get_note(note_id):
    note = Note.query.get(note_id)

    if note:
        return {"status": "success", 'data': {'note': note.to_dict()}}
    else:
        return {"status": "fail", 'message': f'Note with id {note_id} not found'}, 404

@utils.csrf.exempt
def delete_note(note_id):
    note = Note.query.get(note_id)

    if note:
        db.session.delete(note)
        db.session.commit()
        return {'status': 'success', 'message': 'Note deleted successfully'}
    else:
        return {"status": "fail", 'message': f'Note with id {note_id} not found'}, 404


def get_notes():
    page = request.args.get('page', default=1, type=int)
    per_page = request.args.get('limit', default=10, type=int)

    notes = Note.query.paginate(page=page, per_page=per_page, error_out=False)

    note_list = [note.to_dict() for note in notes.items]
    result = {
        'status': 'success',
        'notes': note_list,
        'page': notes.pages,
        'limit': notes.per_page,
        'results': len(note_list)
    }

    return result

Registering Routes in the App

To complete our setup, we will register the CRUD endpoints to invoke the route handler functions we created earlier. Additionally, since our frontend application will be running on a different domain and will interact with the Flask API, we need to configure the server with Cross-Origin Resource Sharing (CORS) to allow requests from our frontend app.

To get started, open your terminal and enter the following command to install the Flask-Cors package:


pip install flask-cors

In order for the CSRF protection to work, you need to add a SECRET_KEY variable to the .env file. This secret key will be used by the Flask-WTF extension to generate the CSRF token. To accomplish this, follow the steps below:

  1. Open the .env file.
  2. Add a new line and enter SECRET_KEY=<your_secret_key>, replacing <your_secret_key> with your desired secret key.
  3. Save the changes to the .env file.

.env


POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=6500
POSTGRES_USER=admin
POSTGRES_PASSWORD=password123
POSTGRES_DB=flask_db

PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=password123

SQLALCHEMY_DATABASE_URI=postgresql://admin:password123@localhost:6500/flask_db
SECRET_KEY=my_ultra_secure_secret

Finally, open the src/__init__.py file and replace its current content with the code provided below:

src/__init__.py


from src.notes_routes import get_note, get_notes, create_note, update_note, delete_note
from flask import Flask, request
from flask_cors import CORS
from dotenv import load_dotenv
import os
from src.models import db, migrate
import src.utils as utils


def handle_error(error):
    response = {
        'status': 'error',
        'message': str(error)
    }
    return response, 500


def create_app():
    app = Flask(__name__, instance_relative_config=True)

    load_dotenv()  # Load environment variables from .env file
    database_uri = os.getenv('SQLALCHEMY_DATABASE_URI')
    app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
    app.config['SQLALCHEMY_DATABASE_URI'] = database_uri

    db.app = app
    db.init_app(app)

    migrate.init_app(app, db)
    utils.csrf.init_app(app)

    @app.get('/api/healthchecker')
    def healthchecker():
        return {"status": "success", "message": "Build RESTful API with Flask and SQLAlchemy"}

    app.route('/api/notes', strict_slashes=False, methods=['GET'])(get_notes)
    app.route('/api/notes', strict_slashes=False,
              methods=['POST'])(create_note)
    app.route('/api/notes/<string:note_id>',
              methods=['PATCH'])(update_note)
    app.route('/api/notes/<string:note_id>', methods=['GET'])(get_note)
    app.route('/api/notes/<string:note_id>', methods=['DELETE'])(delete_note)

    CORS(app, resources={r"/*": {"origins": "http://localhost:3000",
                                 "methods": ["GET", "POST", "PATCH", "DELETE"],
                                 "supports_credentials": True}})

    app.register_error_handler(Exception, handle_error)

    @app.errorhandler(404)
    def handle_not_found_error(e):
        response = {
            'status': 'fail',
            'message': f"route: '{request.path}' not found on this server"
        }
        return response, 404

    return app

Let’s discuss the updates made to the src/__init__.py file:

  1. We imported the csrf instance from the src.utils module and initialized it using utils.csrf.init_app(app). This sets up the CSRF protection for the Flask app.
  2. We registered the CRUD routes to their respective route handler functions. Each route is assigned the corresponding HTTP methods and endpoint.
  3. Additionally, we configured the Flask app with CORS to allow requests from the frontend application. The CORS configuration specifies the allowed origins, methods, and enables support for credentials.

Once you’ve made the necessary changes to the src/__init__.py file, you can start the Flask development server by running the command flask run. Before starting the server, ensure that the PostgreSQL Docker container is running and that you have successfully applied the SQLAlchemy migrations to the database.

To test the API endpoints individually, you can make use of the Note App.postman_collection.json file available in the project’s GitHub repository. You can download or clone the repository and import this Postman collection into either Postman or the Thunder Client VS Code extension. This collection includes all the requests used for testing the API.

If you prefer to test the API with a frontend application, please refer to the instructions provided in the “Running the Flask App with a Frontend Application” section.

Conclusion

And that concludes our tutorial! Throughout this article, we have explored the process of building a REST API in Flask with CRUD functionality. I hope you have found this guide informative and useful.

If you have any questions or feedback, please feel free to leave them in the comments section below. Your input is highly appreciated. Thank you for reading!