Sunday, March 15, 2026

How to Build REST API with Flask

by David Chen
How To Build Rest Api With Flask

How to Build REST API with Flask

To build a REST API with Flask, you define routes that map to HTTP methods (GET, POST, PUT, DELETE) using decorators. Utilize jsonify to return JSON responses and request to access incoming JSON payloads. This setup allows for rapid development of lightweight, maintainable web services using Python’s flexibility.

Metric Details
Core Dependencies Flask (>=2.0), Werkzeug (>=2.0), Jinja2 (>=3.0)
Python Compatibility Python 3.7+ (Recommended: 3.9+)
Request Processing (GET/POST) O(1) for route matching (hash table lookup), O(N) for data parsing (N=payload size)
Memory Footprint (Idle) ~10-30 MB for a basic application (can scale with routes/data)
Typical Latency (Local GET) ~5-15 ms (without database interaction, optimized for 1-5 concurrent requests)
Concurrency Model WSGI (synchronous by default), supports async views with Flask 2.0+ (using asyncio)

The Senior Dev Hook

When I first started building REST APIs with Flask, my biggest mistake was underestimating the importance of input validation and clear error handling. I’d rush to get the core logic working, only to find my endpoints crashing with obscure 500 errors when receiving malformed JSON or unexpected parameters. It was a painful lesson in API robustness; treating external input as inherently untrustworthy and providing precise feedback to the client is paramount for a production-grade API.

Under the Hood Logic

Flask, at its core, is a WSGI application. This means it’s designed to process HTTP requests and return HTTP responses. When a request hits a Flask application, Werkzeug, Flask’s underlying WSGI toolkit, parses the incoming HTTP request. Flask then uses its routing system to match the URL path and HTTP method (GET, POST, PUT, DELETE) to a specific view function. This function executes the application logic, typically interacting with a database or other services. The view function then constructs a response, often using jsonify to serialize Python dictionaries or lists into JSON, which Flask then sends back to the client. The entire process occurs within an application context and a request context, which provide access to request-specific data like the incoming JSON payload (via request.json) or query parameters.

Step-by-Step Implementation

Let’s build a simple API for managing books. We’ll start with an in-memory list for simplicity, but in a real application, you’d connect to a database.

1. Project Setup

First, create a project directory, set up a virtual environment, and install Flask.


mkdir flask_books_api
cd flask_books_api
python3 -m venv venv
source venv/bin/activate  # On Windows, use `venv\Scripts\activate`
pip install Flask

Create a requirements.txt file:


Flask

2. Create the Flask Application (app.py)

Create a file named app.py. This will contain our Flask application and API endpoints.


from flask import Flask, request, jsonify, abort

app = Flask(__name__)

# In-memory data store for books
# In a real application, this would be a database
books = [
    {"id": 1, "title": "The Hitchhiker's Guide to the Galaxy", "author": "Douglas Adams"},
    {"id": 2, "title": "1984", "author": "George Orwell"}
]
next_book_id = 3 # Manually manage IDs for simplicity

# Helper function to get a book by ID
def get_book_by_id(book_id):
    return next((book for book in books if book["id"] == book_id), None)

# Helper function to generate a new ID
def generate_new_id():
    global next_book_id
    current_id = next_book_id
    next_book_id += 1
    return current_id

@app.route('/books', methods=['GET'])
def get_books():
    """
    Retrieves all books.
    GET /books
    """
    return jsonify(books) # Returns the list of books as JSON

@app.route('/books/<int:book_id>', methods=['GET'])
def get_book(book_id):
    """
    Retrieves a single book by its ID.
    GET /books/{book_id}
    """
    book = get_book_by_id(book_id)
    if book is None:
        abort(404, description=f"Book with ID {book_id} not found.") # Return 404 if not found
    return jsonify(book)

@app.route('/books', methods=['POST'])
def add_book():
    """
    Adds a new book.
    POST /books
    Request Body: {"title": "New Title", "author": "New Author"}
    """
    if not request.json or not 'title' in request.json or not 'author' in request.json:
        abort(400, description="Missing 'title' or 'author' in request body.") # Bad request for missing data
    
    new_book = {
        "id": generate_new_id(),
        "title": request.json['title'],
        "author": request.json['author']
    }
    books.append(new_book)
    return jsonify(new_book), 201 # Return 201 Created status code

@app.route('/books/<int:book_id>', methods=['PUT'])
def update_book(book_id):
    """
    Updates an existing book by its ID.
    PUT /books/{book_id}
    Request Body: {"title": "Updated Title", "author": "Updated Author"}
    """
    book = get_book_by_id(book_id)
    if book is None:
        abort(404, description=f"Book with ID {book_id} not found.")
    
    if not request.json:
        abort(400, description="Request body must be JSON.")

    # Update title if provided
    if 'title' in request.json:
        book['title'] = request.json['title']
    # Update author if provided
    if 'author' in request.json:
        book['author'] = request.json['author']
    
    # If no valid fields were provided for update
    if not ('title' in request.json or 'author' in request.json):
        abort(400, description="No valid fields to update (e.g., 'title', 'author').")

    return jsonify(book)

@app.route('/books/<int:book_id>', methods=['DELETE'])
def delete_book(book_id):
    """
    Deletes a book by its ID.
    DELETE /books/{book_id}
    """
    global books
    initial_length = len(books)
    books = [book for book in books if book["id"] != book_id] # Filter out the book to delete
    
    if len(books) == initial_length:
        abort(404, description=f"Book with ID {book_id} not found.")
    
    return jsonify({"message": f"Book with ID {book_id} deleted."}) # Return success message

# Global error handler for 404 (Not Found)
@app.errorhandler(404)
def not_found(error):
    return jsonify({"error": error.description}), 404

# Global error handler for 400 (Bad Request)
@app.errorhandler(400)
def bad_request(error):
    return jsonify({"error": error.description}), 400

# Run the Flask app
if __name__ == '__main__':
    app.run(debug=True) # debug=True for development, auto-reloads and provides debugger

3. Running the API

From your terminal in the project directory:


export FLASK_APP=app.py # On Windows, use `set FLASK_APP=app.py`
export FLASK_ENV=development # Enables debug mode, auto-reloads
flask run

You should see output indicating the server is running, typically on http://127.0.0.1:5000.

4. Testing the Endpoints (using cURL or Postman)

  • GET All Books:
    
    curl http://127.0.0.1:5000/books
            
  • GET Specific Book:
    
    curl http://127.0.0.1:5000/books/1
            
  • POST (Add a Book):
    
    curl -X POST -H "Content-Type: application/json" -d '{"title": "Brave New World", "author": "Aldous Huxley"}' http://127.0.0.1:5000/books
            
  • PUT (Update a Book):
    
    curl -X PUT -H "Content-Type: application/json" -d '{"author": "A. Huxley"}' http://127.0.0.1:5000/books/3
            
  • DELETE (Remove a Book):
    
    curl -X DELETE http://127.0.0.1:5000/books/3
            

What Can Go Wrong (Troubleshooting)

  • 404 Not Found: This usually means your URL path doesn’t match any defined route, or you’re requesting a resource (like a book ID) that doesn’t exist. Check for typos in the URL, ensure parameters like <int:book_id> are correctly used, and verify the resource’s existence.
  • 405 Method Not Allowed: If you’re trying to POST to an endpoint that only accepts GET requests, Flask will return 405. Always ensure your @app.route decorator’s methods argument explicitly lists all allowed HTTP methods for that route.
  • 400 Bad Request: This typically indicates a problem with the request body. Common causes include:
    • Missing required fields (e.g., "title" or "author" in a POST request).
    • Malformed JSON payload (e.g., syntax errors).
    • Not setting the Content-Type: application/json header for POST/PUT requests. Flask’s request.json will be None if this header is incorrect or missing.
  • TypeError: Object of type <SomeObject> is not JSON serializable: This happens when you try to pass an object to jsonify that it doesn’t know how to convert to JSON (e.g., a custom class instance, a datetime object without prior conversion). Ensure all data returned by jsonify is composed of basic Python types (dicts, lists, strings, numbers, booleans, None).
  • CORS Issues: If you’re building a frontend application that tries to access your Flask API from a different domain (e.g., frontend on localhost:3000, API on localhost:5000), you’ll encounter Cross-Origin Resource Sharing (CORS) errors. For development, you can use the Flask-CORS extension to quickly enable CORS headers. For production, carefully configure allowed origins.

Performance & Best Practices

While Flask is excellent for rapid development and moderate-scale APIs, here are considerations for larger applications:

  • When NOT to use Bare Flask: For very large, complex APIs requiring extensive schema validation, automatic OpenAPI documentation, or highly opinionated authentication/authorization mechanisms, bare Flask can be cumbersome. Frameworks built on top of Flask like Flask-RESTful or Flask-Smorest (which integrates with Marshmallow for validation and Webargs for argument parsing) provide more structure. For purely async, high-performance APIs, consider frameworks like FastAPI.
  • Production Deployment: Never run Flask’s built-in development server (app.run(debug=True)) in production. Use a robust WSGI server like Gunicorn or uWSGI behind a reverse proxy like Nginx. These servers handle concurrency and process management efficiently.
  • Database Integration: Use an ORM like SQLAlchemy (often with Flask-SQLAlchemy) for managing database interactions. This abstracts away raw SQL, making your code more maintainable and secure.
  • Input Validation: Implement robust input validation. Libraries like Marshmallow or Pydantic (if you’re embracing type hints) are invaluable for defining schemas and validating incoming request data, preventing bad data from entering your system and providing clear error messages to clients.
  • Modularization with Blueprints: As your API grows, use Flask Blueprints to organize routes, views, and templates into reusable, modular components. This keeps your application structure clean and manageable.
  • Error Handling: Implement consistent, global error handlers for common HTTP errors (400, 401, 403, 404, 500) that return standardized JSON error responses.
  • Logging: Configure proper logging using Python’s logging module to track requests, errors, and application state in production environments.
  • Authentication & Authorization: For securing endpoints, integrate libraries like Flask-HTTPAuth for basic authentication or Flask-JWT-Extended for JWT-based authentication.

For more on this, Check out more Web Development Tutorials.

Author’s Final Verdict

Flask remains my go-to choice for prototyping and building lightweight to medium-sized RESTful services due to its minimalist design and immense flexibility. Its unopinionated nature means I can choose precisely the libraries I need for validation, ORM, or authentication, allowing for highly optimized and focused solutions. For projects where “getting it done” efficiently without sacrificing control is key, Flask, when combined with good architectural practices, is a powerful and performant framework. Just remember my early lesson: prioritize input validation and clear error reporting from day one.

Have any thoughts?

Share your reaction or leave a quick response — we’d love to hear what you think!

Related Posts

Leave a Comment