DEV Community

Cover image for Python CRUD Rest API using Flask, SQLAlchemy, Postgres, Docker, Docker Compose
Francesco Ciulla
Francesco Ciulla

Posted on • Edited on

Python CRUD Rest API using Flask, SQLAlchemy, Postgres, Docker, Docker Compose

Let's create a CRUD Rest API in Python, using:

  • Flask (Python web framework)
  • SQLAlchemy (ORM)
  • Postgres (database)
  • Docker (containerization)
  • Docker Compose (to run the application and the database in containers)

If you prefer a video version:

All the code is available in the GitHub repository (link in the video description): https://youtube.com/live/fHQWTsWqBdE


🏁 Intro

Here is a schema of the architecture of the application we are going to create:

crud, read, update, delete, to a flask app and postgres service, connected with docker compose. Postman and tableplus to test it

We will create 5 endpoints for basic CRUD operations:

  • Create
  • Read all
  • Read one
  • Update
  • Delete

Here are the steps we are going through:

  1. Create a Flask application using SQLALchemy as an ORM.
  2. Dockerize the Flask application writing a Dockerfile and a docker-compose.yml file to run the application and the database
  3. Run the Postgres database in a container using Docker Compose, and test it with TablePlus
  4. Run the Flask application in a container using Docker Compose, and test it with Postman

We will go with a step-by-step guide, so you can follow along.


🏁 Create a Flask application using SQLALchemy as an ORM

Create a new folder:



mkdir flask-crud-api


Enter fullscreen mode Exit fullscreen mode

step into the folder:



cd flask-crud-api


Enter fullscreen mode Exit fullscreen mode

Open the folder with your favorite IDE. I am using VSCode, so I will use the command:



code .


Enter fullscreen mode Exit fullscreen mode

We need just 4 files for the Flask application, including containerization.

You can create these files in different ways. One of them is to create them manually, the other one is to create them with the command line:



touch requirements.txt app.py Dockerfile docker-compose.yml


Enter fullscreen mode Exit fullscreen mode

Your folder structure should look like this:

folder structure - Build a CRUD Rest API in Python using Flask, SQLAlchemy, Postgres, Docker


πŸ—’οΈrequirements.txt file



The requirements.txt file contains all the dependencies of the project. In our case we will need just 3.

Let's add them to the requirements.txt file:



flask
psycopg2-binary
Flask-SQLAlchemy


Enter fullscreen mode Exit fullscreen mode

Short explanation of the dependencies:

flask is the Python web framework we are gonna use.

psycopg2-binary is the driver to make the connection with the Postgres database.

Flask-SQLAlchemy is the ORM to make the queries to the database.


🐍 app.py file



The app.py file is the main file of the application: it contains all the endpoints and the logic of the application.

Populate the app.py file as follows:



from flask import Flask, request, jsonify, make_response
from flask_sqlalchemy import SQLAlchemy
from os import environ

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = environ.get('DB_URL')
db = SQLAlchemy(app)

class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

    def json(self):
        return {'id': self.id,'username': self.username, 'email': self.email}

db.create_all()

#create a test route
@app.route('/test', methods=['GET'])
def test():
  return make_response(jsonify({'message': 'test route'}), 200)


# create a user
@app.route('/users', methods=['POST'])
def create_user():
  try:
    data = request.get_json()
    new_user = User(username=data['username'], email=data['email'])
    db.session.add(new_user)
    db.session.commit()
    return make_response(jsonify({'message': 'user created'}), 201)
  except e:
    return make_response(jsonify({'message': 'error creating user'}), 500)

# get all users
@app.route('/users', methods=['GET'])
def get_users():
  try:
    users = User.query.all()
    return make_response(jsonify([user.json() for user in users]), 200)
  except e:
    return make_response(jsonify({'message': 'error getting users'}), 500)

# get a user by id
@app.route('/users/<int:id>', methods=['GET'])
def get_user(id):
  try:
    user = User.query.filter_by(id=id).first()
    if user:
      return make_response(jsonify({'user': user.json()}), 200)
    return make_response(jsonify({'message': 'user not found'}), 404)
  except e:
    return make_response(jsonify({'message': 'error getting user'}), 500)

# update a user
@app.route('/users/<int:id>', methods=['PUT'])
def update_user(id):
  try:
    user = User.query.filter_by(id=id).first()
    if user:
      data = request.get_json()
      user.username = data['username']
      user.email = data['email']
      db.session.commit()
      return make_response(jsonify({'message': 'user updated'}), 200)
    return make_response(jsonify({'message': 'user not found'}), 404)
  except e:
    return make_response(jsonify({'message': 'error updating user'}), 500)

# delete a user
@app.route('/users/<int:id>', methods=['DELETE'])
def delete_user(id):
  try:
    user = User.query.filter_by(id=id).first()
    if user:
      db.session.delete(user)
      db.session.commit()
      return make_response(jsonify({'message': 'user deleted'}), 200)
    return make_response(jsonify({'message': 'user not found'}), 404)
  except e:
    return make_response(jsonify({'message': 'error deleting user'}), 500)


Enter fullscreen mode Exit fullscreen mode

Explanation:

We are importing:

  • Flask as a framework
  • request to handle the HTTP
  • jsonify to handle the json format, not native in Python
  • make_response to handle the HTTP responses
  • flask_sqlalchemy to handle the db queries
  • environ to handle the environment variables

We are creating Flask app, configuring the database bu setting an environment variable called 'DB_URL'. We will set it later in the docker-compose.yml file.

Then we are creating a User class with an id, a username and an email. the id will be autoincremented automatically by SQLAlchemy when we will create the users. the __tablename__ = 'users' line is to define the name of the table in the database

An important line is db.create_all(). This will synchronize the database with the model defined, for example creating an "users" table.

Then we have 6 endpoints

  • test: just a test route
  • create a user: create a user with a username and an email
  • get all users: get all the users in the database
  • get one user: get one user by id
  • update one user: update one user by id
  • delete one user: delete one user by id

All the routes have error handling, for example if the user is not found, we will return a 404 HTTP response.

You can check a video-explanation here


🐳 Dockerize the Flask application



Let's populate the Dockerfile :

Dockerfile:



FROM python:3.6-slim-buster

WORKDIR /app

COPY requirements.txt ./

RUN pip install -r requirements.txt

COPY . .

EXPOSE 4000

CMD [ "flask", "run", "--host=0.0.0.0", "--port=4000"]


Enter fullscreen mode Exit fullscreen mode

FROM sets the base image to use. In this case we are using the python 3.6 slim buster image

WORKDIR sets the working directory inside the image

COPY requirements.txt ./ copies the requirements.txt file to the working directory

RUN pip install -r requirements.txt installs the requirements

COPY . . copies all the files in the current directory to the working directory

EXPOSE 4000 exposes the port 4000

CMD [ "flask", "run", "--host=0.0.0.0", "--port=4000"] sets the command to run when the container starts


🐳🐳Docker compose



The term "Docker compose" might be a bit confusing because it's referred both to a file and to a set of CLI commands. Here we will use the term to refer to the file.

Populate the docker-compose.yml file:



version: "3.9"

services:
  flask_app:
    container_name: flask_app
    image: dockerhub-flask_live_app:1.0.0
    build: .
    ports:
      - "4000:4000"
    environment:
      - DB_URL=postgresql://postgres:postgres@flask_db:5432/postgres
    depends_on:
      - flask_db
  flask_db:
    container_name: flask_db
    image: postgres:12
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_DB=postgres
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata: {}



Enter fullscreen mode Exit fullscreen mode

We just defined 2 services: flask_app and flask_db

flask_app is the Flask application we just dockerized

flask_db is a Postgres container, to store the data. We will use the official Postgres image

Explanation:

version is the version of the docker-compose file. We are using the verwion 3.9

services is the list of services (containers) we want to run. In this case, we have 2 services: flask_app and flask_db

container_name is the name of the container. It's not mandatory, but it's a good practice to have a name for the container. Containers find each other by their name, so it's important to have a name for the containers we want to communicate with.

image is the name of the image we want to use. I recommend replacing "dockerhub-" with YOUR Dockerhub account (it's free).

build is the path to the Dockerfile. In this case, it's the current directory, so we are using .

ports is the list of ports we want to expose. In this case, we are exposing the port 4000 of the flask_app container, and the port 5432 of the flask_db container. The format is host_port:container_port

depends_on is the list of services we want to start before this one. In this case, we want to start the flask_db container before the flask_app container

environment is to define the environment variables. for the flask_app, we will have a database url to configure the configuration. For the flask_db container, we will have the environment variables we have to define when we wan to use the Postgres container (we can't change the keys here, because we are using the Postgres image, defined by the Postgres team).

volumes in the flask_db defines a named volume we will use for persistency. Containers are ephimerals by definition, so we need this additional feature to make our data persist when the container will be removed (a container is just a process).

volumes at the end of the file is the list of volumes we want to create. In this case, we are creating a volume called pgdata. The format is volume_name: {}


πŸ‘Ÿ Run the Postgres container and test it with TablePlus



To run the Postgres container, type:



docker compose up -d flask_db


Enter fullscreen mode Exit fullscreen mode

The -d flag is to run the container in detached mode, so it will run in the background.

You should see something like this:

docker downlading image - Build a CRUD Rest API in Python using Flask, SQLAlchemy, Postgres, Docker

Docker is pulling (downloading) the Postgres image on our local machine and it's running a container based on that Postgres image.

To check if the container is running, type:



docker compose logs


Enter fullscreen mode Exit fullscreen mode

If everything is ok, you should see something like this:

databse log

If the last line is LOG: database system is ready to accept connections, it means that the container is running and the Postgres server is ready to accept connections.

But to be sure, let's make another test.

To show all the containers (running and stopped ones) type:



docker ps -a


Enter fullscreen mode Exit fullscreen mode

The output should be similar to this:

one container running

Now, to test the db connection, we can use any tool we want. Personally, I use TablePlus.

Use the following configuration:

Host: localhost

Port: 5432

User: postgres

Password: postgres

Database: postgres

Tableplus interface

Then hit "Test" (at the bottom-right).

If you get the message "connection is OK" you are good to go.

TAbleplus, OK connection

You can also click "Connect" and you will see an empty database. This is correct.

Tableplus empty but connected db


πŸ”¨ Build and run the Flask application



Now, let's build and run the Flask application.

Let's go back to the folder where the docker-compose.yml is located and type:



docker compose build


Enter fullscreen mode Exit fullscreen mode

This should BUILD the flask_app image, with the name defined in the "image" value. In my case it's francescoxx/flask_live_app:1.0.0 because that's my Dockerhub username. You should replace "francescoxx" with your Dockerhub username.

You can also see all the steps docker did to build the image, layer by layer. You might recognize some of them, because we defined them in the Dockerfile.

docker build

Now, to check if the image has been built successfully, type:



docker images


Enter fullscreen mode Exit fullscreen mode

We should see a similar result, with the image we just built:

2 docker images


βš—οΈ Run the flask_app service



We are almost done, but one last step is to run a container based on the image we just built.

To do that, we can just type:



docker compose up flask_app


Enter fullscreen mode Exit fullscreen mode

In this case we don't use the -d flag, because we want to see the logs in the terminal.

We should see something like this:

docker compose up command


πŸ” Test the application



Let's test our application. First of all, let's just go to any browser and visit localhost:4000/test

You shoulds see this result:

test endpoint

(note that if you visit localhost:4000 you get an error because there is no route associated to this endpoint, but by getting an error is a good thing, because it means that the server is running!)

Now it's time to test all the endpints using Postman. Feel free to use any tool you want.

If we make a GET request to localhost:4000/users we will get an empty array. This is correct

GET request to localhost:4000/users


πŸ“ Create a user



Now let's create a user, making a POST request to localhost:4000/users with the body below as a request body:

POST request to localhost:4000/users

Let's crete another one:

POST request to localhost:4000/users

One more:

POST request to localhost:4000/users


πŸ“ Get all users



Now, let's make a GET request to localhost:4000/users to get all the users:

GET request to localhost:4000/users

We just created 3 users.


πŸ“ Get a specific user



If you want to get a specific user, you can make a GET request to localhost:4000/users/<user_id>.

For example, to get the user with id 2, you can make a GET request to localhost:4000/users/2

GET request to localhost:4000/users/2


πŸ“ Update a user



If you want to update a user, you can make a PUT request to localhost:4000/users/<user_id>.

For example, to update the user with id 2, you can make a PUT request to localhost:4000/users/2 with the body below as a request body:

PUT request to localhost:4000/users/2

To check if the user has been updated, you can make a GET request to localhost:4000/users/2

GET request to localhost:4000/users/2


πŸ“ Delete a user



To delete a user, you can make a DELETE request to localhost:4000/users/<user_id>.

For Example, to delete the user with id 2, you can make a DELETE request to localhost:4000/users/2

DELETE request to localhost:4000/users/2

To check if the user has been deleted, you can make a GET request to localhost:4000/users

GET request to localhost:4000/users

As you can see the user with id 2 is not there anymore.


🏁 Conclusion



We made it! We have built a CRUD rest API in Python, using Flask, SQLAlchemy, Postgres, Docker and Docker compose.

This is just an example, but you can use this as a starting point to build your own application.

If you prefer a video version:

All the code is available in the GitHub repository (link in the video description): https://youtube.com/live/fHQWTsWqBdE

That's all.

If you have any question, drop a comment below.

Francesco

Top comments (21)

Collapse
 
ndevadas profile image
ndevadas

This is a great article.

In my code I am using the connection like the following and using sql statements in api's

conn = psycopg2.connect(host="0.0.0.0",
                        database="lakedistrict",
                        user="root",
                        password="root1!")
return conn
Enter fullscreen mode Exit fullscreen mode

but when I run the app python code I am getting following error: Any suggestion? Thank you.

Error:

psycopg2.OperationalError: connection to server at "0.0.0.0", port 5432 failed: Connection refused

Collapse
 
francescoxx profile image
Francesco Ciulla

are you running this in a container or without Docker?

Collapse
 
ndevadas profile image
ndevadas

Outside container this code work fine. When I combine this in the container with postgress I get the error.

The compose file:

version: '3'

services:
postgres:
image: postgres:13
restart: always
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: root1!
POSTGRES_DB: lakedistrict

ports:
  - "5432:5432"
volumes:
  - postgres_data:/var/lib/postgresql/data
Enter fullscreen mode Exit fullscreen mode

pgadmin:
image: dpage/pgadmin4
restart: always
environment:
PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org
PGADMIN_DEFAULT_PASSWORD: admin
PGADMIN_LISTEN_PORT: 80
ports:
- "9090:80"
depends_on:
- postgres
links:
- postgres

app:
build: .
ports:
- 8100:8200
depends_on:
- postgres
volumes:
- ./static/css:/static/css
- ./static/data:/static/data
- ./static/js:/static/js
- ./static/json:/static/json
- ./static/svg:/static/svg
- ./templates:/templates

volumes:
postgres_data:

THANK YOU

Thread Thread
 
ndevadas profile image
ndevadas

Also, forgot to mention, I downloaded your code and dropped them in to containers and it worked fine. My issue is I am creating some dynamic full text search in the API search function and needed some flexibility. So I avoided using SQLAlchemy.

Thread Thread
 
francescoxx profile image
Francesco Ciulla

I cna't see where you define the user and password for the app. you defined them in the postgres volume so you should either use a DATABASE_URL environment variable or pass them in some ways.

Thread Thread
 
ndevadas profile image
ndevadas

I added that in the app.py code like so:

def db_connection():
conn = None
while not conn:
try:
conn = psycopg2.connect(host="0.0.0.0",
database="lakedistrict",
user="root",
password="root1!")

        print("Database connection successful")

    except BaseException as e:
        print(e)
        time.sleep(5)

return conn
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
francescoxx profile image
Francesco Ciulla

change host="0.0.0.0" to host="db" and try again

Thread Thread
 
ndevadas profile image
ndevadas

That worked Francesco. I really appreciate. Thank you so much.

Thread Thread
 
francescoxx profile image
Francesco Ciulla

I knew it! I say it clearly here in the video but you had a slighlt different setup (basically hardcoding the values instead of passing them using docker compose)

https://youtu.be/njNXTM6L0wc?si=JijupWLhY57-D6aB&t=812)

Thread Thread
 
ndevadas profile image
ndevadas

I sent a message yesterday not sure you saw that.

Yes, when I changed host="0.0.0.0" to host="postgres" the container system worked as expected.

I really appreciate your help and Thank you very much.

Thread Thread
 
francescoxx profile image
Francesco Ciulla

ye saw it, you are welcome!

Collapse
 
scottlarsen profile image
Scott Larsen

Excellent tutorial so far, a few notes for those trying to follow (or for updating the article). For those getting 500 errors when trying to add a user via Postman, in the Postman POST Request, under Headers, set one with the key as Content-Type and value as application/json. And a tiny note that when testing the database with TablePlus, all of my fields turned green, which I'm guessing means success (I didn't get the text notice shown in the article that the test was successful).

Collapse
 
francescoxx profile image
Francesco Ciulla

This is a great point!

Collapse
 
krishnaa192 profile image
krishnaa192

Is it good method to add all code in one file??Is it not preferred method add models and routes in different file,like Django?

Collapse
 
francescoxx profile image
Francesco Ciulla

it depends on the complexity of the project. for bigger ones, for sure! in this case, I go with the simpliciy of having less files and I focus on the dockerization of the project

Collapse
 
sumitsaurabh927 profile image
Sumit Saurabh

Very well explained Francesco!

Collapse
 
francescoxx profile image
Francesco Ciulla

Thank you Sumit!

Collapse
 
chrisgreening profile image
Chris Greening

Cheers thanks for sharing Francesco - this is a super detailed tutorial I love it!

Gotta love me some Flask 😎 definitely one of my fave Python frameworks

Collapse
 
francescoxx profile image
Francesco Ciulla

you are welcome Chris!

Collapse
 
fortunatus profile image
Fortunatus Adegoke

Nice πŸ‘ step by step explanation on the API CRUD using flask.
Really like it.

Collapse
 
francescoxx profile image
Francesco Ciulla

thank you! more are coming!