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
Post a Comment