Getting Started with Flask: Building Your First RESTful API in Python

Getting Started with Flask: Building Your First RESTful API in Python

In this post we’ll walk through a step-by-step guide on how to build your first RESTful API in Python using the Flask framework.

Setup Python and Install Flask

Follow these steps to create a new Python virtual environment:

# create a new directory
mkdir getting-started-with-flask
cd getting-started-with-flask

# create the virtual environment
python -m venv .venv

# activate the virtual environment
. .venv/bin/activate # macos/linux
.venv/Scripts/Activate.bat # windows

Now that we have everything we need installed let's discuss a very simple project we will write to demonstrate how we can write a RESTful API.

Project: Employee Management API

We're going to take the simple concept of managing employee information and turn it into an API. This could be easily integrated into a frontend system, or any other system. Let's start by defining some requirements:

  • be able to add a new employee when they join the company
  • be able to edit an existing employee (changing their home address for example)
  • be able to delete an existing employee (perhaps after they leave the company)
  • be able to list all employees

The model

A good place to start is to define a data model which will represent the required fields and the type of information they will hold. Let's create our first script called model.py:

# model.py
from datetime import datetime
from dataclasses import dataclass
from typing import Optional
from enum import Enum

class Role(str, Enum):
  EMPLOYEE = 'employee'
  MANAGER  = 'manager'

  def __str__(self):
    return self.value

@dataclass
class Employee:
  first_name    : str
  last_name     : str
  date_of_birth : datetime
  start_date    : datetime
  job_title     : str
  role          : Role
  end_date      : Optional[datetime] = None
  id            : Optional[int] = None

Using the model we can create an employee:

employee = Employee(
  first_name    = 'Joe',
  last_name     = 'Bloggs',
  date_of_birth = datetime(1988, 12, 5),
  start_date    = datetime(2024, 8, 15),
  job_title     = 'Python Developer',
  role          = Role.EMPLOYEE
)

The API

The API will use our new model to perform the required functions we need. As this is a RESTful API we’ll follow the standard approach here:

  • Stateless: we won’t be storing any session related information, in other words we won’t be remembering anything other than what is provided in each request
  • URL-based: the URL (or path) provides the identification for the request (for example: /user/1 identifies the user #1)
  • HTTP methods: certain HTTP methods are used for specific actions (GET to retrieve, POST to create something new, PUT to update and DELETE to remove)

There are many other characteristics of RESTful APIs but these are the most important ones for our simple example. For reference these are the remaining characteristics:

  • Scalability: designed to scale and perform under different loads, therefore can be easily replicated across many nodes without additional infrastructure
  • JSON/XML format: typically used for request and response message formats
  • Client-Server Separation: The API (the server) performs a very clearly defined function, whereas the client handles the visual elements
  • Cacheable: Retrieval objects can be remembered to improve performance later on
  • Layering: additional layers can be introduced to provide additional non-functional features such as security and caching

With that in mind we can now define our API:

# api.py
from datetime import datetime
from flask import Flask, request, jsonify
from model import Employee, Role

app = Flask(__name__)
employees = {}

@app.route('/', methods=['GET'])
def list_employees():
  return jsonify(list(employees.values()))

@app.route('/<int:employee_id>', methods=['GET'])
def get_employee(employee_id: int):
  if employee_id not in employees:
    return {'message': 'Employee not found'}, 404

  return jsonify(employees[employee_id])

@app.route('/', methods=['POST'])
def add_employee():
  data = request.json
  id = len(employees)

  employee = Employee(
    id            = id,
    first_name    = data['first_name'],
    last_name     = data['last_name'],
    date_of_birth = datetime.fromisoformat(data['date_of_birth']),
    start_date    = datetime.fromisoformat(data['start_date']),
    job_title     = data['job_title'],
    role          = Role(data['role'])
  )

  employees[id] = employee
  return jsonify(employee)

@app.route('/<int:employee_id>', methods=['PUT'])
def update_employee(employee_id: int):
  if employee_id not in employees:
    return {'message': 'Employee not found'}, 404
  
  if employees[employee_id].end_date:
    return {'message': 'Employee is no longer active'}, 400

  for key in request.json:
    if key in ['id', 'end_date']:
      continue

    setattr(employees[employee_id], key, request.json[key])

  return jsonify(employees[employee_id])

@app.route('/<int:employee_id>', methods=['DELETE'])
def delete_employee(employee_id: int):
  if employee_id not in employees:
    return {'message': 'Employee not found'}, 404

  employees[employee_id].end_date = datetime.now()

  return jsonify(employees[employee_id])

Now that we have written all the endpoints we can now run some tests. Start by running the server:

# store a new employee
> curl -H 'Content-Type: application/json' -X POST -d '{"first_name":"Joe","last_name":"Bloggs","date_of_birth":"1988-03-19","start_date":"2024-08-28","job_title":"Python Developer","role":"employee"}' http://127.0.0.1:5000/
{"date_of_birth":"Sat, 19 Mar 1988 00:00:00 GMT","end_date":null,"first_name":"Joe","id":0,"job_title":"Python Developer","last_name":"Bloggs","role":"employee","start_date":"Wed, 28 Aug 2024 00:00:00 GMT"}

# list employees
> curl -H 'Content-Type: application/json' -X GET http://127.0.0.1:5000
[{"date_of_birth":"Sat, 19 Mar 1988 00:00:00 GMT","end_date":null,"first_name":"Joe","id":0,"job_title":"Python Developer","last_name":"Bloggs","role":"employee","start_date":"Wed, 28 Aug 2024 00:00:00 GMT"}]

# change a single field
> curl -H 'Content-Type: application/json' -X PUT -d '{"first_name":"Bob"}' http://127.0.0.1:5000/0
{"date_of_birth":"Sat, 19 Mar 1988 00:00:00 GMT","end_date":null,"first_name":"Bob","id":0,"job_title":"Python Developer","last_name":"Bloggs","role":"employee","start_date":"Wed, 28 Aug 2024 00:00:00 GMT"}

# delete an employee
> curl -X DELETE http://127.0.0.1:5000/0
{"date_of_birth":"Sat, 19 Mar 1988 00:00:00 GMT","end_date":"Tue, 03 Sep 2024 22:33:02 GMT","first_name":"Bob","id":0,"job_title":"Python Developer","last_name":"Bloggs","role":"employee","start_date":"Wed, 28 Aug 2024 00:00:00 GMT"}
flask --app api run

The --app api refers to the script called api.py accessible within the current directory. Then in a new terminal window run the following to test:

Persistence

You’ll notice that when the server is restarted all changes have been lost. We’re only using a Python list in this example which is stored in memory. To complete this project we’ll add persistence as a simple CSV file stored within the same directory as the 2 Python scripts we already have:

  • CSV file created only when needed (the first new employee creates the file)
  • If CSV file exists when the server starts, then the contents are loaded into memory
  • For simplicity, every add, save or delete will save the entire list of employees to file
# db.py
import os
import csv
from typing import Dict
from model import Employee

def get_employees() -> Dict[int, Employee]:
  if not os.path.exists('employees.csv'):
    return {}

  with open('employees.csv', 'r', newline='') as file:
    reader = csv.DictReader(file)
    return {row['id']:Employee(**row) for row in reader}

def save_employees(items: Dict[int, Employee]):
  data = [i.__dict__ for i in items.values()]
  keys = data[0].keys()

  with open('employees.csv', 'w', newline='') as output_file:
      dict_writer = csv.DictWriter(output_file, keys)
      dict_writer.writeheader()
      dict_writer.writerows(data)

Then, we just need to adjust our api.py to use get_employees when the server starts:

# api.py (snippet)
from db import get_employees, save_employees
# ...
# employees = {}
employees = get_employees()
# ...

and save_employees whenever a POSTPUT or DELETE is called:

# api.py (snippet)
@app.route('/', methods=['POST'])
def add_employee():
  # ...
  save_employees(employees)
  return jsonify(employee)

@app.route('/<int:employee_id>', methods=['PUT'])
def update_employee(employee_id: int):
  # ...
  save_employees(employees)
  return jsonify(employees[employee_id])

@app.route('/<int:employee_id>', methods=['DELETE'])
def delete_employee(employee_id: int):
  # ...
  save_employees(employees)
  return jsonify(employees[employee_id])

Not the most groundbreaking database in the world but simple enough for this example. This could very easily be replaced with an integration to an actual database usable in production. To test this:

  • close the server (control + C) and re-run
  • use the curl commands above to create, update, delete some records
  • notice the changes getting saved to the CSV file
  • restart the server again
  • notice employees added before you restarted are now being returned.

End

With this fairly trivial example and a few scripts of code we’ve been able to implement an employee API. Although a bit basic, with a bit more work and functionality it could be used in production. To further enhance this you could think about the following:

  • integration with a real database
  • validation - checking the inputs and providing descriptive responses
  • authentication and authorisation - ensuring only certain people can access and make changes
  • model enhancements - exposing more data fields