Build Your First REST API with Flask

Part 2 of the Python for Web & APIs track. Last updated: September 2026.

Part 1 taught you to consume APIs. Now flip the perspective: build your own. Flask is the classic Python micro-framework — small, explicit, perfect for learning how web APIs actually work under the hood. By the end of this post you will have a working REST API for a book catalog with full CRUD, proper status codes, and clean error handling.

Setup

Use a virtual environment (always, for web projects):

python -m venv venv
source venv/bin/activate      # Windows: venv\Scripts\activate
pip install flask

Your first Flask app

A route is just a Python function mapped to a URL:

from flask import Flask

app = Flask(__name__)

@app.get("/hello")
def hello():
    return {"message": "Hello from Flask!"}   # dicts become JSON automatically

if __name__ == "__main__":
    app.run(debug=True)    # dev server ONLY — never in production

Run it with python app.py, then visit http://127.0.0.1:5000/hello — you get {"message": "Hello from Flask!"}. Returning a dict from a view is all it takes to serve JSON.

Dynamic URLs: path parameters

Capture parts of the URL with <...> placeholders:

@app.get("/books/<int:book_id>")
def get_book(book_id):
    # Flask converts book_id to int for you; /books/abc won't match
    return {"id": book_id, "title": f"Book #{book_id}"}

Converters like int:, float:, and path: validate and convert for free.

Reading query parameters

Filters like ?author=tolkien live in request.args:

from flask import request

@app.get("/search")
def search():
    author = request.args.get("author", "")      # default if missing
    limit = request.args.get("limit", default=10, type=int)  # converted to int
    return {"author": author, "limit": limit}

Reading a JSON body

POST/PUT payloads arrive as JSON — request.get_json() parses them:

@app.post("/echo")
def echo():
    data = request.get_json()      # None if body isn't valid JSON
    if data is None:
        return {"error": "Expected a JSON body"}, 400
    return {"you_sent": data}, 200

Note the pattern: return a (body, status_code) tuple to control the status code.

Project: a book catalog API

Time to build something real — a books API with in-memory storage (a real database comes in a later track; the API design is identical):

from flask import Flask, request

app = Flask(__name__)

books = {
    1: {"id": 1, "title": "Dune", "author": "Frank Herbert", "year": 1965},
    2: {"id": 2, "title": "Neuromancer", "author": "William Gibson", "year": 1984},
}
next_id = 3

@app.get("/books")
def list_books():
    author = request.args.get("author")
    result = list(books.values())
    if author:
        result = [b for b in result if author.lower() in b["author"].lower()]
    return {"books": result, "count": len(result)}

@app.get("/books/<int:book_id>")
def get_book(book_id):
    book = books.get(book_id)
    if book is None:
        return {"error": f"No book with id {book_id}"}, 404
    return book

@app.post("/books")
def create_book():
    global next_id
    data = request.get_json()
    if not data or "title" not in data or "author" not in data:
        return {"error": "Body must be JSON with 'title' and 'author'"}, 400
    book = {"id": next_id, "title": data["title"],
            "author": data["author"], "year": data.get("year")}
    books[next_id] = book
    next_id += 1
    return book, 201          # 201 Created, not 200

@app.put("/books/<int:book_id>")
def update_book(book_id):
    book = books.get(book_id)
    if book is None:
        return {"error": f"No book with id {book_id}"}, 404
    data = request.get_json() or {}
    book.update({k: v for k, v in data.items() if k in ("title", "author", "year")})
    return book

@app.delete("/books/<int:book_id>")
def delete_book(book_id):
    if book_id not in books:
        return {"error": f"No book with id {book_id}"}, 404
    del books[book_id]
    return "", 204           # 204 No Content — deleted successfully

Try it end to end

With the server running, exercise the API from another terminal with curl (or the requests skills from Part 1):

# List all books
# curl http://127.0.0.1:5000/books
# -> {"books": [...], "count": 2}

# Create one (note the 201)
# curl -X POST http://127.0.0.1:5000/books \
#   -H "Content-Type: application/json" \
#   -d '{"title": "Snow Crash", "author": "Neal Stephenson", "year": 1992}'

# Filter by author
# curl "http://127.0.0.1:5000/books?author=stephenson"

# Fetch a missing book -> 404 with a JSON error, not an HTML page
# curl http://127.0.0.1:5000/books/999

JSON error handlers

By default Flask renders errors as HTML — useless for an API. Register JSON handlers so every error speaks JSON:

@app.errorhandler(404)
def not_found(e):
    return {"error": "Not found — check the URL"}, 404

@app.errorhandler(405)
def method_not_allowed(e):
    return {"error": "That HTTP method isn't allowed here"}, 405

@app.errorhandler(500)
def server_error(e):
    return {"error": "Something broke on our side"}, 500

Input validation, the Flask way

Flask won't validate your JSON for you — do it explicitly at the door of every endpoint, before touching business logic:

def validate_book(data):
    errors = []
    if not isinstance(data, dict):
        return ["Body must be a JSON object"]
    if not data.get("title") or not isinstance(data["title"], str):
        errors.append("'title' is required and must be a string")
    if not data.get("author") or not isinstance(data["author"], str):
        errors.append("'author' is required and must be a string")
    year = data.get("year")
    if year is not None and not isinstance(year, int):
        errors.append("'year' must be an integer if provided")
    return errors

@app.post("/books")
def create_book():
    data = request.get_json(silent=True)   # None instead of raising on bad JSON
    errors = validate_book(data)
    if errors:
        return {"error": "Invalid input", "details": errors}, 400
    ...

Verbose, but explicit. (Part 3 shows how FastAPI makes this disappear with type hints.)

Key takeaways

  • A Flask route is a function mapped to a URL — returning a dict serves JSON.
  • CRUD maps to HTTP: GET read, POST create (201), PUT update, DELETE remove (204).
  • Use the right status codes: 201 for created, 400 for bad input, 404 for missing resources.
  • Register JSON error handlers — never leak HTML error pages from an API.
  • app.run(debug=True) is for development only — production servers come in Part 4.

Next in this series: FastAPI Crash Course: Modern, Fast Python APIs — the same API, rebuilt with type hints, automatic validation, and free documentation.

Comments

Popular posts from this blog

Java Banking Finance Services and Insurance (BFSI) domain interview questions

JSP Servlet Interview Questions For Freshers Series 1

Java program to check even or odd number