Building Your First Simple Flask API with a Database

Welcome, aspiring web developers! Have you ever wondered how apps talk to each other, or how websites store all that information you see? The answer often involves an API and a database. In this guide, we’re going to build a simple but powerful web API using Flask, a popular Python framework, and connect it to a database to store our information.

Don’t worry if these terms sound a bit daunting. We’ll break everything down into easy-to-understand steps, perfect for beginners!

What Are We Building Today?

Today, we’ll create a basic “To-Do List” API. This API will allow us to:
* Create new to-do items.
* Read all existing to-do items.
* Read a single specific to-do item.
* Update an existing to-do item.
* Delete a to-do item.

This covers the fundamental “CRUD” operations (Create, Read, Update, Delete) that are the backbone of many applications.

Let’s Define Some Key Terms

Before we dive into the code, let’s quickly clarify some important concepts:

  • API (Application Programming Interface): Think of an API as a waiter in a restaurant. You (the client) tell the waiter (the API) what you want (e.g., “get me all to-do items”), and the waiter goes to the kitchen (the server/database) to get it for you and brings it back. It’s a set of rules and tools that allow different software applications to communicate with each other.
  • Flask: Flask is a “micro web framework” for Python. This means it provides the essential tools to build web applications without getting in your way with too many rigid rules. It’s lightweight, flexible, and very popular for building APIs.
  • Database: A database is an organized collection of data, stored and accessed electronically. It’s like a highly organized digital filing cabinet where our to-do items will live permanently, even after our Flask application stops running. For this tutorial, we’ll use SQLite, which is a simple, file-based database perfect for small projects and learning.
  • ORM (Object-Relational Mapper): This is a fancy term for a tool that helps us interact with our database using Python objects instead of raw SQL queries. We’ll use SQLAlchemy (and Flask-SQLAlchemy, an extension for Flask) to make database operations much easier and more Pythonic.
  • JSON (JavaScript Object Notation): This is a lightweight format for storing and transporting data. It’s commonly used when data is sent from a server to a web page or, in our case, between our API and its clients. It looks very similar to Python dictionaries.

Prerequisites

To follow along, you’ll need:
1. Python 3: Make sure you have Python 3 installed on your system. You can download it from python.org.
2. pip: This is Python’s package installer, usually included with Python 3. We’ll use it to install Flask and other libraries.
3. A text editor or IDE (like VS Code, PyCharm, Atom, or Sublime Text).

Setting Up Your Project

It’s good practice to create a virtual environment for your Python projects. This keeps the dependencies (libraries) for each project separate, preventing conflicts.

  1. Create a Project Folder:
    First, create a new folder for our project. You can name it flask_todo_api.

    bash
    mkdir flask_todo_api
    cd flask_todo_api

  2. Create a Virtual Environment:
    Inside your project folder, run this command to create a virtual environment named venv:

    bash
    python -m venv venv

  3. Activate the Virtual Environment:
    You need to activate this environment so that any packages you install only go into this project’s environment.

    • On macOS/Linux:
      bash
      source venv/bin/activate
    • On Windows (Command Prompt):
      bash
      venv\Scripts\activate.bat
    • On Windows (PowerShell):
      bash
      venv\Scripts\Activate.ps1

      You’ll notice (venv) appearing at the beginning of your command prompt, indicating that the virtual environment is active.
  4. Install Required Packages:
    Now, with the virtual environment active, install Flask and Flask-SQLAlchemy:

    bash
    pip install Flask Flask-SQLAlchemy

    Flask-SQLAlchemy is an extension that simplifies using SQLAlchemy (our ORM) with Flask.

Building Our Flask Application

Now that our environment is set up, let’s start coding! Create a file named app.py in your flask_todo_api folder.

1. Initialize Flask and Database

First, we’ll import necessary libraries, initialize our Flask app, and set up our database connection.

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

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # This disables a warning

db = SQLAlchemy(app)
  • Flask: The main class for our web application.
  • request: Used to access incoming request data (like data sent in POST or PUT requests).
  • jsonify: A helper function to turn Python dictionaries into JSON responses.
  • SQLAlchemy(app): Connects our Flask app to the database via SQLAlchemy.

2. Define the Database Model

Next, we need to tell SQLAlchemy what our Todo items should look like in the database. This is our “model.”

class Todo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    description = db.Column(db.String(200), nullable=True)
    completed = db.Column(db.Boolean, default=False)

    def __repr__(self):
        return f'<Todo {self.id}: {self.title}>'

    # A method to easily convert our Todo object to a dictionary (for JSON response)
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'description': self.description,
            'completed': self.completed
        }
  • db.Model: This tells SQLAlchemy that Todo is a database model.
  • db.Column: Defines a column in our database table.
  • db.Integer, db.String, db.Boolean: These are the data types for our columns.
  • primary_key=True: id will be unique and automatically generated, acting as the main identifier for each todo item.
  • nullable=False: Means this field cannot be empty.
  • default=False: Sets a default value if none is provided.
  • to_dict(): This is a custom method we added to make it easy to convert a Todo object into a Python dictionary, which jsonify can then turn into JSON.

3. Create Database Tables

Before we can interact with the database, we need to create the tables based on our model. We’ll do this once when the application starts or when we run a specific command. For simplicity, we can do it inside our app.py file, but in larger apps, you might use a separate script or Flask CLI commands.

with app.app_context():
    db.create_all()

app.app_context(): Flask needs to know which application instance it’s dealing with to perform certain operations, especially those related to extensions like Flask-SQLAlchemy. app_context provides that context. db.create_all() then creates the tables.

4. Implement API Endpoints (Routes)

Now let’s define the “routes” or “endpoints” that our API will respond to. Each route will correspond to an HTTP method (GET, POST, PUT, DELETE) and a URL path.

@app.route('/todos', methods=['GET', 'POST'])
def handle_todos():
    if request.method == 'POST':
        # Create a new Todo item
        data = request.json # Get JSON data from the request body
        if not data or not data.get('title'):
            return jsonify({'message': 'Title is required'}), 400

        new_todo = Todo(
            title=data['title'],
            description=data.get('description'), # Use .get() to safely access optional fields
            completed=data.get('completed', False) # Default to False if not provided
        )
        db.session.add(new_todo) # Add the new todo object to the database session
        db.session.commit() # Commit the transaction to save it to the database
        return jsonify(new_todo.to_dict()), 201 # Return the created item with 201 Created status

    else: # GET request
        # Get all Todo items
        all_todos = Todo.query.all() # Query all Todo objects from the database
        return jsonify([todo.to_dict() for todo in all_todos]) # Return a list of dictionaries as JSON

@app.route('/todos/<int:todo_id>', methods=['GET', 'PUT', 'DELETE'])
def handle_single_todo(todo_id):
    todo = Todo.query.get_or_404(todo_id) # Find todo by ID, or return 404 if not found

    if request.method == 'GET':
        # Get a specific Todo item
        return jsonify(todo.to_dict())

    elif request.method == 'PUT':
        # Update a specific Todo item
        data = request.json
        if not data:
            return jsonify({'message': 'No data provided for update'}), 400

        # Update fields if they are provided in the request
        if 'title' in data:
            todo.title = data['title']
        if 'description' in data:
            todo.description = data['description']
        if 'completed' in data:
            todo.completed = data['completed']

        db.session.commit() # Commit changes to the database
        return jsonify(todo.to_dict())

    else: # DELETE request
        # Delete a specific Todo item
        db.session.delete(todo) # Mark the object for deletion
        db.session.commit() # Commit the deletion
        return jsonify({'message': 'Todo item deleted successfully'}), 204 # 204 No Content status

Let’s break down what’s happening in these routes:
* @app.route(...): This is a “decorator” that registers the function handle_todos or handle_single_todo to be called when a request matches the specified URL path and HTTP method.
* methods=['GET', 'POST']: Specifies which HTTP methods this route should handle.
* request.json: This automatically parses incoming JSON data from the request body into a Python dictionary.
* db.session.add(new_todo): Stages the new Todo object to be added to the database.
* db.session.commit(): Saves all staged changes (additions, updates, deletions) to the database permanently.
* Todo.query.all(): Fetches all Todo objects from the database.
* Todo.query.get_or_404(todo_id): Fetches a single Todo object by its id. If no Todo with that ID is found, it automatically sends a 404 Not Found error.
* jsonify(some_dict): Converts a Python dictionary (some_dict) into a JSON formatted string and sets the correct HTTP Content-Type header.
* return jsonify(new_todo.to_dict()), 201: We return the JSON representation of the created item along with an HTTP status code 201 (Created), which is good practice. Other codes like 200 (OK), 400 (Bad Request), 204 (No Content) are also used.

5. Run the Application

Finally, add the standard block to run your Flask application:

if __name__ == '__main__':
    app.run(debug=True) # Run the development server
  • if __name__ == '__main__':: This ensures the code inside this block only runs when you execute app.py directly (not when it’s imported as a module).
  • app.run(debug=True): Starts the Flask development server. debug=True provides helpful error messages and automatically reloads the server when you make code changes. Never use debug=True in a production environment!

The Complete app.py File

Here’s the full code for your app.py file:

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
import os # We'll use this for the database path

app = Flask(__name__)

basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'todo.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # This disables a warning

db = SQLAlchemy(app)

class Todo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    description = db.Column(db.String(200), nullable=True)
    completed = db.Column(db.Boolean, default=False)

    def __repr__(self):
        return f'<Todo {self.id}: {self.title}>'

    # A method to easily convert our Todo object to a dictionary (for JSON response)
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'description': self.description,
            'completed': self.completed
        }

with app.app_context():
    db.create_all()


@app.route('/todos', methods=['GET', 'POST'])
def handle_todos():
    if request.method == 'POST':
        # Create a new Todo item
        data = request.json # Get JSON data from the request body
        if not data or not data.get('title'):
            return jsonify({'message': 'Title is required'}), 400

        new_todo = Todo(
            title=data['title'],
            description=data.get('description'),
            completed=data.get('completed', False)
        )
        db.session.add(new_todo)
        db.session.commit()
        return jsonify(new_todo.to_dict()), 201

    else: # GET request
        # Get all Todo items
        all_todos = Todo.query.all()
        return jsonify([todo.to_dict() for todo in all_todos])

@app.route('/todos/<int:todo_id>', methods=['GET', 'PUT', 'DELETE'])
def handle_single_todo(todo_id):
    todo = Todo.query.get_or_404(todo_id)

    if request.method == 'GET':
        # Get a specific Todo item
        return jsonify(todo.to_dict())

    elif request.method == 'PUT':
        # Update a specific Todo item
        data = request.json
        if not data:
            return jsonify({'message': 'No data provided for update'}), 400

        if 'title' in data:
            todo.title = data['title']
        if 'description' in data:
            todo.description = data['description']
        if 'completed' in data:
            todo.completed = data['completed']

        db.session.commit()
        return jsonify(todo.to_dict())

    else: # DELETE request
        # Delete a specific Todo item
        db.session.delete(todo)
        db.session.commit()
        return jsonify({'message': 'Todo item deleted successfully'}), 204

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

Running and Testing Your API

  1. Run the Flask app:
    Make sure your virtual environment is active, then run:

    bash
    python app.py

    You should see output similar to this, indicating the server is running:
    * Serving Flask app 'app'
    * Debug mode: on
    WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
    * Running on http://127.0.0.1:5000
    Press CTRL+C to quit
    * Restarting with stat
    * Debugger is active!
    * Debugger PIN: XXX-XXX-XXX

    Your API is now live at http://127.0.0.1:5000 (which is localhost:5000).

  2. Test with curl (or Postman/Insomnia):
    curl is a command-line tool for making HTTP requests. Open a new terminal window (keep the server running in the first one), and make sure your virtual environment is not active in this new terminal if you are using curl from outside the venv.

    • Create a To-Do item (POST):
      bash
      curl -X POST -H "Content-Type: application/json" -d '{"title": "Learn Flask API", "description": "Finish the Flask API tutorial"}' http://127.0.0.1:5000/todos

      You should get a response like:
      json
      {"completed": false, "description": "Finish the Flask API tutorial", "id": 1, "title": "Learn Flask API"}

      The id might be different if you’ve already created other items.

    • Create another To-Do item:
      bash
      curl -X POST -H "Content-Type: application/json" -d '{"title": "Buy groceries", "completed": true}' http://127.0.0.1:5000/todos

    • Get all To-Do items (GET):
      bash
      curl http://127.0.0.1:5000/todos

      You should see a list of all items you created:
      json
      [{"completed": false, "description": "Finish the Flask API tutorial", "id": 1, "title": "Learn Flask API"}, {"completed": true, "description": null, "id": 2, "title": "Buy groceries"}]

    • Get a specific To-Do item (GET) (replace 1 with the ID of an item):
      bash
      curl http://127.0.0.1:5000/todos/1

      Response:
      json
      {"completed": false, "description": "Finish the Flask API tutorial", "id": 1, "title": "Learn Flask API"}

    • Update a To-Do item (PUT) (let’s mark the first item as completed):
      bash
      curl -X PUT -H "Content-Type: application/json" -d '{"completed": true}' http://127.0.0.1:5000/todos/1

      Response:
      json
      {"completed": true, "description": "Finish the Flask API tutorial", "id": 1, "title": "Learn Flask API"}

    • Delete a To-Do item (DELETE):
      bash
      curl -X DELETE http://127.0.0.1:5000/todos/2

      Response:
      json
      {"message": "Todo item deleted successfully"}

      If you now try to GET all todos, you’ll see only the first one remains.

Congratulations! You’ve successfully built and tested your first Flask API connected to a database!

Conclusion

You’ve taken a significant step in your web development journey. You learned what APIs and databases are, how to set up a Flask project, define a database model with Flask-SQLAlchemy, and create API endpoints to perform CRUD operations.

This simple to-do list API forms the foundation for countless other web services. From here, you can explore adding authentication, handling more complex data relationships, deploying your API, or even building a front-end application to interact with it. Keep experimenting and building!

Comments

Leave a Reply