nimbuscode.dev/blog/posts/building-restful-apis-with-flask
C:\> cat BLOG/RESTFUL_APIs.md

Building Robust RESTful APIs with Python and Flask

Introduction

In the modern web development landscape, RESTful APIs serve as the backbone for communication between frontend applications and backend services. Whether you're building a mobile app, a single-page application, or integrating with third-party services, a well-designed API is crucial for success.

Flask, a lightweight and flexible Python web framework, is an excellent choice for building APIs due to its simplicity and extensibility. It provides just enough structure without imposing rigid constraints, allowing developers to craft APIs that perfectly match their requirements.

This tutorial will guide you through the process of building robust, production-ready RESTful APIs with Flask. We'll cover everything from basic routing to advanced topics like authentication, validation, and testing, with practical code examples that you can adapt for your own projects.

REST Principles

Before diving into code, it's important to understand the core principles of REST (Representational State Transfer):

  • Statelessness: Each request from client to server must contain all the information needed to understand and process the request.
  • Resource-Based: APIs are organized around resources (data or objects), each with a unique identifier (URI).
  • Standard HTTP Methods: Use GET, POST, PUT, DELETE, etc. for standard operations on resources.
  • Representation: Resources can have multiple representations (JSON, XML, etc.).
  • Hypermedia: Responses should include links to related resources (HATEOAS).

Adhering to these principles makes your API more intuitive, easier to use, and compatible with standard tools and libraries.

Setting Up Flask

Let's start by setting up a basic Flask application with the necessary extensions:

pip install flask flask-restful flask-sqlalchemy flask-marshmallow marshmallow-sqlalchemy

Now, let's create a basic project structure:

my_api/
├── app.py
├── config.py
├── models/
│   └── __init__.py
├── resources/
│   └── __init__.py
├── schemas/
│   └── __init__.py
└── requirements.txt

Here's a basic setup in app.py:

from flask import Flask
from flask_restful import Api
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow

app = Flask(__name__)
app.config.from_object('config')

# Initialize extensions
db = SQLAlchemy(app)
ma = Marshmallow(app)
api = Api(app)

# Import and register resources
# ...

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

And in config.py:

import os

# Database configuration
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False

# Security
SECRET_KEY = os.environ.get('SECRET_KEY', 'my-secret-key')  # Change in production!

# API settings
JSON_SORT_KEYS = False

Basic API Routing

Now, let's create a simple REST API for a bookstore. First, we'll define a Book model in models/__init__.py:

from app import db
from datetime import datetime

class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(255), nullable=False)
    author = db.Column(db.String(255), nullable=False)
    description = db.Column(db.Text)
    price = db.Column(db.Float, nullable=False)
    publication_date = db.Column(db.Date)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def __repr__(self):
        return f'<Book {self.title} by {self.author}>'

Next, create a schema in schemas/__init__.py:

from app import ma
from models import Book

class BookSchema(ma.SQLAlchemySchema):
    class Meta:
        model = Book
        
    id = ma.auto_field()
    title = ma.auto_field()
    author = ma.auto_field()
    description = ma.auto_field()
    price = ma.auto_field()
    publication_date = ma.auto_field()
    created_at = ma.auto_field()
    updated_at = ma.auto_field()

book_schema = BookSchema()
books_schema = BookSchema(many=True)

Now, let's define resources in resources/__init__.py:

from flask import request
from flask_restful import Resource
from app import db
from models import Book
from schemas import book_schema, books_schema

class BookListResource(Resource):
    def get(self):
        """Get all books"""
        books = Book.query.all()
        return books_schema.dump(books)
    
    def post(self):
        """Create a new book"""
        json_data = request.get_json()
        if not json_data:
            return {'message': 'No input data provided'}, 400
        
        try:
            book = Book(
                title=json_data['title'],
                author=json_data['author'],
                description=json_data.get('description', ''),
                price=json_data['price'],
                publication_date=json_data.get('publication_date')
            )
            db.session.add(book)
            db.session.commit()
            return book_schema.dump(book), 201
        except Exception as e:
            db.session.rollback()
            return {'message': str(e)}, 400

class BookResource(Resource):
    def get(self, book_id):
        """Get a single book"""
        book = Book.query.get_or_404(book_id)
        return book_schema.dump(book)
    
    def put(self, book_id):
        """Update a book"""
        book = Book.query.get_or_404(book_id)
        json_data = request.get_json()
        
        if not json_data:
            return {'message': 'No input data provided'}, 400
            
        try:
            if 'title' in json_data:
                book.title = json_data['title']
            if 'author' in json_data:
                book.author = json_data['author']
            if 'description' in json_data:
                book.description = json_data['description']
            if 'price' in json_data:
                book.price = json_data['price']
            if 'publication_date' in json_data:
                book.publication_date = json_data['publication_date']
                
            db.session.commit()
            return book_schema.dump(book)
        except Exception as e:
            db.session.rollback()
            return {'message': str(e)}, 400
    
    def delete(self, book_id):
        """Delete a book"""
        book = Book.query.get_or_404(book_id)
        try:
            db.session.delete(book)
            db.session.commit()
            return '', 204
        except Exception as e:
            db.session.rollback()
            return {'message': str(e)}, 400

Finally, register these resources in app.py:

from resources import BookResource, BookListResource

# Register resources
api.add_resource(BookListResource, '/api/books')
api.add_resource(BookResource, '/api/books/')

Now you have a basic CRUD API for books. You can test it with tools like Postman or curl:

# Create a book
curl -X POST http://localhost:5000/api/books \
     -H "Content-Type: application/json" \
     -d '{"title":"Flask API Development","author":"Michael Houghton","price":29.99}'
     
# Get all books
curl http://localhost:5000/api/books

# Get a specific book
curl http://localhost:5000/api/books/1

# Update a book
curl -X PUT http://localhost:5000/api/books/1 \
     -H "Content-Type: application/json" \
     -d '{"price":34.99}'
     
# Delete a book
curl -X DELETE http://localhost:5000/api/books/1

Request Validation

Proper validation is essential for ensuring data integrity and security. Let's enhance our schema with validation:

from app import ma
from models import Book
from marshmallow import validate, validates, ValidationError

class BookSchema(ma.SQLAlchemySchema):
    class Meta:
        model = Book
        
    id = ma.auto_field(dump_only=True)  # Read-only field
    title = ma.auto_field(required=True, validate=validate.Length(min=1, max=255))
    author = ma.auto_field(required=True, validate=validate.Length(min=1, max=255))
    description = ma.auto_field()
    price = ma.auto_field(required=True, validate=validate.Range(min=0))
    publication_date = ma.auto_field()
    created_at = ma.auto_field(dump_only=True)  # Read-only field
    updated_at = ma.auto_field(dump_only=True)  # Read-only field
    
    @validates('price')
    def validate_price(self, value):
        if value <= 0:
            raise ValidationError('Price must be greater than zero.')

Now, let's update our resource to use this validation:

class BookListResource(Resource):
    def post(self):
        """Create a new book"""
        json_data = request.get_json()
        if not json_data:
            return {'message': 'No input data provided'}, 400
        
        try:
            # Validate and deserialize input
            data = book_schema.load(json_data)
            
            # Create new book
            book = Book(**data)
            db.session.add(book)
            db.session.commit()
            
            return book_schema.dump(book), 201
        except ValidationError as err:
            return {'message': 'Validation error', 'errors': err.messages}, 422
        except Exception as e:
            db.session.rollback()
            return {'message': str(e)}, 400

Comments (0)

Sort by: