Building a robust RESTful API with Python has never been easier thanks to Flask’s lightweight design and SQLAlchemy’s powerful ORM capabilities. In this guide you’ll learn how to create a production‑ready Flask API, connect it to a relational database using SQLAlchemy, and expose clean CRUD endpoints that developers love to consume. Follow each step, copy the code snippets, and you’ll have a fully functional API ready for deployment in under an hour.
Why Choose Flask and SQLAlchemy for a REST API?
Flask is a micro‑framework that gives you just enough tools to get started while staying flexible enough for complex projects. SQLAlchemy abstracts away raw SQL, letting you interact with databases using Python classes and objects. Together they provide:
- Fast development cycles with minimal boilerplate.
- Clear separation of concerns – routing, business logic, and data access.
- Scalability from small prototypes to large, production‑grade services.
- Excellent community support and extensive documentation.
Prerequisites
Before diving into code, make sure you have the following installed on your development machine:
- Python 3.9+ (preferably via
pyenvor a virtual environment). - pip (Python package manager).
- Git (optional, but useful for version control).
- A relational database – SQLite works for demos, while PostgreSQL or MySQL are recommended for production.
Once ready, create a fresh virtual environment and install the required packages:
python -m venv venv
source venv/bin/activate # On Windows use: venv\Scripts\activate
pip install Flask SQLAlchemy Flask-Migrate marshmallow
Project Structure
A clean folder layout makes the codebase easier to navigate and maintain. Below is a recommended structure for a Flask‑SQLAlchemy API:
my_flask_api/
├── app/
│ ├── __init__.py # Application factory
│ ├── models.py # SQLAlchemy models
│ ├── schemas.py # Marshmallow schemas (optional)
│ ├── routes.py # API endpoints
│ └── config.py # Configuration settings
├── migrations/ # Database migration scripts (Flask‑Migrate)
├── tests/ # Unit and integration tests
├── .env # Environment variables (secret keys, DB URL)
└── run.py # Entry point
Creating the Flask Application Factory
Using an application factory gives you flexibility for testing, different configurations, and future extensions. Save the following in app/__init__.py:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
def create_app(config_object='app.config.DevelopmentConfig'):
app = Flask(__name__)
app.config.from_object(config_object)
# Initialise extensions
db.init_app(app)
migrate.init_app(app, db)
# Register blueprints (routes)
from .routes import api_bp
app.register_blueprint(api_bp, url_prefix='/api')
return app
Configuring the Database
Store configuration values in app/config.py. For a production‑ready setup, keep secrets out of source control by using environment variables.
import os
class BaseConfig:
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key')
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(BaseConfig):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///dev.db')
class ProductionConfig(BaseConfig):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL')
Defining Models with SQLAlchemy
Models represent tables in your relational database. Below is a simple Todo model placed in app/models.py:
from . import db
from datetime import datetime
class Todo(db.Model):
__tablename__ = 'todos'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120), nullable=False)
description = db.Column(db.Text, nullable=True)
completed = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f''
Serializing Data with Marshmallow (Optional)
While you can manually build JSON responses, Marshmallow simplifies serialization and validation. Create app/schemas.py:
from marshmallow import Schema, fields
class TodoSchema(Schema):
id = fields.Int(dump_only=True)
title = fields.Str(required=True)
description = fields.Str(allow_none=True)
completed = fields.Bool()
created_at = fields.DateTime(dump_only=True)
Creating CRUD Endpoints
All routes live in app/routes.py. We’ll use a Flask Blueprint to keep the API modular.
from flask import Blueprint, request, jsonify, abort
from . import db
from .models import Todo
from .schemas import TodoSchema
api_bp = Blueprint('api', __name__)
todo_schema = TodoSchema()
todos_schema = TodoSchema(many=True)
# ---------- Create ----------
@api_bp.route('/todos', methods=['POST'])
def create_todo():
json_data = request.get_json()
if not json_data:
abort(400, description='No input data provided')
# Validate & deserialize
data, errors = todo_schema.load(json_data)
if errors:
return jsonify(errors), 422
todo = Todo(**data)
db.session.add(todo)
db.session.commit()
return todo_schema.jsonify(todo), 201
# ---------- Read (list) ----------
@api_bp.route('/todos', methods=['GET'])
def get_todos():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
pagination = Todo.query.paginate(page, per_page, error_out=False)
result = {
'items': todos_schema.dump(pagination.items),
'total': pagination.total,
'page': pagination.page,
'pages': pagination.pages
}
return jsonify(result)
# ---------- Read (single) ----------
@api_bp.route('/todos/<int:todo_id>', methods=['GET'])
def get_todo(todo_id):
todo = Todo.query.get_or_404(todo_id)
return todo_schema.jsonify(todo)
# ---------- Update ----------
@api_bp.route('/todos/<int:todo_id>', methods=['PUT'])
def update_todo(todo_id):
todo = Todo.query.get_or_404(todo_id)
json_data = request.get_json()
if not json_data:
abort(400, description='No input data provided')
data, errors = todo_schema.load(json_data, partial=True)
if errors:
return jsonify(errors), 422
for key, value in data.items():
setattr(todo, key, value)
db.session.commit()
return todo_schema.jsonify(todo)
# ---------- Delete ----------
@api_bp.route('/todos/<int:todo_id>', methods=['DELETE'])
def delete_todo(todo_id):
todo = Todo.query.get_or_404(todo_id)
db.session.delete(todo)
db.session.commit()
return '', 204
Running Migrations
Flask‑Migrate (which wraps Alembic) keeps your database schema in sync with your models.
# Initialise migration directory (run once)
flask db init
# Generate migration script after model changes
flask db migrate -m "Create Todo table"
# Apply migration to the database
flask db upgrade
Make sure the FLASK_APP environment variable points to your entry point (run.py) before executing the commands.
Entry Point (run.py)
This tiny script boots the Flask development server. For production, replace it with a WSGI server like Gunicorn or uWSGI.
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Testing the API with Postman or curl
Once the server is running, you can interact with the endpoints:
- Create a todo:
curl -X POST http://localhost:5000/api/todos -H "Content-Type: application/json" -d '{"title":"Buy groceries","description":"Milk, Eggs, Bread"}' - List todos (paginated):
curl http://localhost:5000/api/todos?page=1&per_page=5 - Update a todo:
curl -X PUT http://localhost:5000/api/todos/1 -H "Content-Type: application/json" -d '{"completed":true}' - Delete a todo:
curl -X DELETE http://localhost:5000/api/todos/1
Postman collections can be exported for team sharing and automated testing.
Best Practices for Production‑Ready Flask APIs
1. Use Blueprints for Modularity
Separate resources (e.g., users, orders, products) into distinct blueprints. This keeps routing tables manageable and encourages reusable code.
Leave a Reply