To build a small REST API with Flask, define routes for a resource, accept and return JSON, give errors explicit HTTP statuses, and test the app through Flask’s test client. This tutorial builds an in-memory book collection with list, create, retrieve, replace, and delete operations. It uses a small app factory so the same application can be configured for testing. The example is intentionally a starting point: it does not add authentication, database persistence, validation libraries, or API versioning.
What this tutorial builds
The API represents books at /api/books. It supports these operations:
As an Amazon Associate I earn from qualifying purchases.
| Request | Purpose | Success response |
|---|---|---|
GET /api/books |
List books | 200 OK with a JSON array |
POST /api/books |
Create a book | 201 Created with the new book |
GET /api/books/<id> |
Retrieve one book | 200 OK with a JSON object |
PUT /api/books/<id> |
Replace a book’s fields | 200 OK with the updated book |
DELETE /api/books/<id> |
Delete a book | 204 No Content |
These are tutorial choices, not a required Flask layout or a universal REST rule. Flask is flexible; its official tutorial demonstrates one structured approach using an application factory. This example keeps the data in memory, so it resets whenever the process restarts.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Install Flask and create the application
Use a virtual environment, then install Flask:
python -m venv .venv
# macOS or Linux:
. .venv/bin/activate
# Windows PowerShell:
.venvScriptsActivate.ps1
python -m pip install Flask
Create a file named app.py. The factory initializes the Flask application and registers its routes. The in-memory dictionary is deliberately simple; it is not suitable for durable or multi-process storage.
#1 Best Overall
from flask import Flask, jsonify, request
def create_app(test_config=None):
app = Flask(__name__)
app.config.from_mapping(SECRET_KEY="dev")
if test_config is not None:
app.config.update(test_config)
# Teaching-only storage: data disappears on restart and is not shared
# across separate worker processes.
app.books = {}
app.next_book_id = 1
@app.get("/api/books")
def list_books():
return list(app.books.values())
@app.post("/api/books")
def create_book():
data = request.get_json(silent=True)
if not isinstance(data, dict):
return {"error": "Expected a JSON object."}, 400
title = data.get("title")
if not isinstance(title, str) or not title.strip():
return {"error": "A non-empty title is required."}, 400
book = {
"id": app.next_book_id,
"title": title.strip(),
"author": data.get("author", ""),
}
app.books[book["id"]] = book
app.next_book_id += 1
return book, 201
@app.get("/api/books/<int:book_id>")
def get_book(book_id):
book = app.books.get(book_id)
if book is None:
return {"error": "Book not found."}, 404
return book
@app.put("/api/books/<int:book_id>")
def replace_book(book_id):
if book_id not in app.books:
return {"error": "Book not found."}, 404
data = request.get_json(silent=True)
if not isinstance(data, dict):
return {"error": "Expected a JSON object."}, 400
title = data.get("title")
if not isinstance(title, str) or not title.strip():
return {"error": "A non-empty title is required."}, 400
book = {
"id": book_id,
"title": title.strip(),
"author": data.get("author", ""),
}
app.books[book_id] = book
return book
@app.delete("/api/books/<int:book_id>")
def delete_book(book_id):
if book_id not in app.books:
return {"error": "Book not found."}, 404
del app.books[book_id]
return "", 204
return app
app = create_app()
Flask converts returned dictionaries and lists to JSON responses. Values in those structures still need to be JSON-serializable: if you later return a database model, convert it to ordinary values such as strings, numbers, lists, and dictionaries first. The official Quickstart explains this behavior in About Responses.
Run the API and send requests
For local development, start Flask from the directory containing app.py:
flask --app app run --debug
Use another terminal to create a book. The request’s JSON content type tells Flask to parse its body as JSON:
Recommended Free Tools
curl -i -X POST http://127.0.0.1:5000/api/books
-H "Content-Type: application/json"
-d '{"title":"The Left Hand of Darkness","author":"Ursula K. Le Guin"}'
The response has status 201 Created and a JSON body similar to:
{
"author": "Ursula K. Le Guin",
"id": 1,
"title": "The Left Hand of Darkness"
}
Then retrieve the collection and the individual record:
curl http://127.0.0.1:5000/api/books
curl http://127.0.0.1:5000/api/books/1
Try a replacement with PUT using a JSON object with a title, then delete the record:
curl -i -X PUT http://127.0.0.1:5000/api/books/1
-H "Content-Type: application/json"
-d '{"title":"The Dispossessed","author":"Ursula K. Le Guin"}'
curl -i -X DELETE http://127.0.0.1:5000/api/books/1
In this example, PUT replaces the fields the resource stores; it is not a partial update. A partial-update operation would need its own defined behavior and request validation.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Keep failures useful and consistent
The example returns an error property for expected client mistakes and missing records. It pairs the JSON body with a meaningful status: malformed or incomplete input gets 400, and an unknown book ID gets 404. A client can inspect the status without having to infer success or failure from the response text.
For larger APIs, centralize error formatting instead of repeating response construction in each route. Flask supports handlers for HTTP exceptions and custom exceptions. When an error handler constructs a response, return the intended status explicitly; registering a handler for a particular code does not automatically apply that code to the response. See Flask’s guide to Returning API Errors as JSON.
Rank #4
This small example only checks that the body is an object and that title is a non-empty string. It does not define a complete schema, field-length limits, or authentication rules. Those are separate API decisions, and should be implemented deliberately rather than assumed to come from Flask.
Why use an application factory?
create_app builds and returns an application instead of configuring the app only as a module-level global. That makes it practical to pass testing or other configuration when creating an instance, and to register routes and extensions in one setup point. A single-file application is fine for a tiny demonstration; the factory becomes more helpful as configuration and tests grow.
As the project expands, move related endpoints into a blueprint and register it inside the factory. For Flask extensions, the documented pattern is to define the extension object separately, then initialize it with the app inside the factory. See the official guide to Application Setup.
Best Value
Add persistence when the API needs it
The in-memory dictionary makes the route behavior easy to see, but it loses all records when the app stops and does not coordinate data between worker processes. For a small persistent learning project, Flask’s tutorial demonstrates SQLite: open a connection using the application context and close it during app-context teardown. SQLite is an option shown by Flask’s tutorial, not a requirement of Flask or a universal production database choice. The tutorial’s database pattern is documented in Define and Access the Database.
Test routes with a separate app instance
Factories let tests create an app with testing configuration, then exercise routes through Flask’s test client without starting a live server. Here is a minimal pytest test in test_app.py:
import pytest
from app import create_app
@pytest.fixture
def client():
app = create_app({"TESTING": True})
return app.test_client()
def test_create_and_retrieve_book(client):
created = client.post(
"/api/books",
json={"title": "Kindred", "author": "Octavia E. Butler"},
)
assert created.status_code == 201
book_id = created.json["id"]
retrieved = client.get(f"/api/books/{book_id}")
assert retrieved.status_code == 200
assert retrieved.json["title"] == "Kindred"
def test_missing_book_is_json_404(client):
response = client.get("/api/books/999")
assert response.status_code == 404
assert response.json == {"error": "Book not found."}
Install the test runner with python -m pip install pytest and run pytest from the project directory. Each fixture-created application starts with an empty in-memory collection, so the tests do not depend on request order. Flask documents the factory fixture and test-client approach in its Testing Flask Applications guide.
Free tools Windows power users keep installed
One-click scans. No signup required.
Use a production server when deploying
The Flask development server and interactive debugger are for local work, not production traffic. Flask’s production guide states: “Do not use the development server when deploying to production.” Use a production WSGI server or a hosting platform configured to run the application. Waitress is one example in the Flask documentation; it is not the only deployment option. Follow the current Deploying to Production guidance for the server and platform you choose.
Before deployment, replace the in-memory data store with persistence appropriate to the application, configure secrets outside source code, and decide how the API should handle authentication, input validation, logging, and operational limits. Those concerns are not supplied automatically by these routes.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




