Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

To build a small REST-style API with Flask, define routes for a resource, accept and return JSON, use HTTP methods to express each operation, and return meaningful status codes. This tutorial builds an in-memory task API, then shows how to organize it with an application factory, test it, and prepare it for deployment. The example is a teaching pattern, not a required Flask layout or a complete production security design.

Flask’s 3.1.x documentation supports returning Python dictionaries and lists as JSON. The official tutorial says, “The tutorial only uses what’s provided by Flask and Python.” Flask documentation

What this example builds

The API manages tasks at /api/tasks. It supports listing and creating tasks, retrieving one task, replacing or partially updating it, and deleting it. The data lives in memory, so it disappears when the process stops; persistence is covered later.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Method Path Purpose Successful response
GET /api/tasks List tasks 200 with a JSON array
POST /api/tasks Create a task 201 with the created task
GET /api/tasks/<task_id> Retrieve one task 200 with a JSON object
PUT /api/tasks/<task_id> Replace a task’s editable fields 200 with the updated task
PATCH /api/tasks/<task_id> Update only supplied fields 200 with the updated task
DELETE /api/tasks/<task_id> Delete a task 204 with no response body

These paths and method semantics are choices for this tutorial. Flask does not require a particular URI scheme, versioning convention, authentication system, database, or validation library.

Create and run a minimal Flask API

Install Flask in your project’s virtual environment, then create app.py:

from flask import Flask, jsonify, request

app = Flask(__name__)
tasks = {}
next_id = 1


def error(message, status):
    return jsonify({"error": message}), status


@app.get("/api/tasks")
def list_tasks():
    return jsonify(list(tasks.values()))


@app.post("/api/tasks")
def create_task():
    global next_id
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return error("Request body must be a JSON object.", 400)

    title = data.get("title")
    if not isinstance(title, str) or not title.strip():
        return error("'title' must be a non-empty string.", 400)

    task = {"id": next_id, "title": title.strip(), "done": False}
    tasks[next_id] = task
    next_id += 1
    return jsonify(task), 201


@app.get("/api/tasks/<int:task_id>")
def get_task(task_id):
    task = tasks.get(task_id)
    if task is None:
        return error("Task not found.", 404)
    return jsonify(task)


@app.put("/api/tasks/<int:task_id>")
def replace_task(task_id):
    task = tasks.get(task_id)
    if task is None:
        return error("Task not found.", 404)

    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return error("Request body must be a JSON object.", 400)
    title = data.get("title")
    done = data.get("done")
    if not isinstance(title, str) or not title.strip():
        return error("'title' must be a non-empty string.", 400)
    if not isinstance(done, bool):
        return error("'done' must be a boolean.", 400)

    task.update({"title": title.strip(), "done": done})
    return jsonify(task)


@app.patch("/api/tasks/<int:task_id>")
def update_task(task_id):
    task = tasks.get(task_id)
    if task is None:
        return error("Task not found.", 404)

    data = request.get_json(silent=True)
    if not isinstance(data, dict) or not data:
        return error("Request body must be a non-empty JSON object.", 400)
    if "title" in data:
        if not isinstance(data["title"], str) or not data["title"].strip():
            return error("'title' must be a non-empty string.", 400)
        task["title"] = data["title"].strip()
    if "done" in data:
        if not isinstance(data["done"], bool):
            return error("'done' must be a boolean.", 400)
        task["done"] = data["done"]
    unknown = set(data) - {"title", "done"}
    if unknown:
        return error("Unsupported field(s): " + ", ".join(sorted(unknown)), 400)
    return jsonify(task)


@app.delete("/api/tasks/<int:task_id>")
def delete_task(task_id):
    if task_id not in tasks:
        return error("Task not found.", 404)
    del tasks[task_id]
    return "", 204

Flask converts returned dictionaries and lists into JSON responses; jsonify is used here where the code also needs to pair a body with a status code. Database models and other complex values are not automatically made JSON-safe: convert them to supported values such as dictionaries, strings, numbers, booleans, lists, or null before returning them. See the Flask Quickstart.

Run the local development server with the Flask command from the project directory:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
flask --app app run --debug

For example, create a task with curl:

curl -i -X POST http://127.0.0.1:5000/api/tasks 
  -H "Content-Type: application/json" 
  -d '{"title":"Write the API","done":false}'

The response should have status 201 and a JSON object containing an assigned id. Then request the collection with curl http://127.0.0.1:5000/api/tasks. The debug server is for local iteration only, not deployment.

Return consistent JSON errors

Clients need both an HTTP status and a predictable response body to distinguish an invalid request from a missing resource or a server failure. The example uses an error key consistently, such as {"error":"Task not found."} with status 404. Flask’s guide groups 400–499 responses as client request or data errors and 500–599 responses as server or application errors.

For larger applications, Flask supports handlers for HTTP errors and custom exceptions. A handler should return the intended status explicitly; registering a handler for a particular code does not automatically set the response status to that code. Avoid returning internal exception details to clients in production. Consult Flask’s error-handling documentation.

Move from one file to an application factory

A single file is convenient for a first experiment. As routes and configuration grow, an application factory keeps creation and configuration explicit. Flask documents factories as useful for testing different configurations and creating multiple configured application instances.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A compact structure could be:

project/
  myapi/
    __init__.py
    routes.py
  tests/
    test_api.py

In myapi/__init__.py, create the app and register routes:

from flask import Flask


def create_app(test_config=None):
    app = Flask(__name__)
    app.config.from_mapping(SECRET_KEY="development-only")

    if test_config is not None:
        app.config.update(test_config)

    from .routes import api
    app.register_blueprint(api, url_prefix="/api")
    return app

Move the route functions into myapi/routes.py and define a blueprint there. For example, start the module with from flask import Blueprint and api = Blueprint("api", __name__), then change route decorators to use @api.get("/tasks") and the corresponding paths. The factory’s /api prefix keeps the earlier URLs intact. Route logic can then be separated from app creation and settings.

When using Flask extensions, the documented pattern is to create an extension object separately and initialize it with the app inside the factory. This avoids binding the extension to only one application instance. Flask’s application factory tutorial shows the pattern in context.

Add persistence when the API needs it

The in-memory dictionary makes the example easy to run but does not retain tasks across restarts and is not a suitable shared store for concurrent production workers. If you need durable storage, choose a database based on the application’s requirements. Flask does not require SQLite; its own tutorial uses SQLite as a starter example.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The official tutorial opens a SQLite connection using the Flask application context and closes it during application-context teardown. That pattern ties connection lifetime to the request or command context rather than leaving connections open indefinitely. Follow the Flask database tutorial for its connection and teardown example. For production data modeling, migrations, backups, transaction handling, and database selection, make explicit choices for the needs of your service rather than treating the tutorial’s starter database as a universal recommendation.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Test the API with a separate app configuration

A factory lets tests create an app configured for testing instead of modifying the development instance. Flask’s testing facilities provide a client that makes requests against the application without starting a network server.

import pytest
from myapi import create_app


@pytest.fixture()
def app():
    return create_app({"TESTING": True})


@pytest.fixture()
def client(app):
    return app.test_client()


def test_create_and_fetch_task(client):
    response = client.post("/api/tasks", json={"title": "Test task"})
    assert response.status_code == 201
    task_id = response.json["id"]

    fetched = client.get(f"/api/tasks/{task_id}")
    assert fetched.status_code == 200
    assert fetched.json["title"] == "Test task"


def test_missing_task_is_json_404(client):
    response = client.get("/api/tasks/999")
    assert response.status_code == 404
    assert response.json == {"error": "Task not found."}

The example tests cover the success path and a client-facing error. Extend the suite to cover invalid JSON, missing or incorrectly typed fields, updates, deletion, and any persistence behavior you add. If tests use a database, arrange isolated test data so that one test’s changes do not affect another. Flask’s testing documentation describes the factory-based fixture pattern and test client.

Deploy with a production WSGI server or hosting platform

Flask’s built-in development server and debugger are designed for local development, not production operation. The Flask production guide states: “Do not use the development server when deploying to production.” Use a production WSGI server or a hosting platform that runs the application in an appropriate production configuration; the guide gives Waitress as one example among multiple deployment options.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Before exposing an API, also make deployment-specific decisions about configuration secrets, authentication, request validation, logging, HTTPS, and operational limits. Those concerns are not supplied automatically by the small route example. Start with the official Deploying to Production guide and the documentation for the server or platform you select.

What Flask does and does not decide for you

Flask supplies routing, request handling, JSON response behavior, error handlers, and testing support. The project structure, persistent store, authentication method, CORS policy, pagination design, schema-validation approach, rate limits, and API documentation tooling are additional design decisions. Optional community extensions or libraries can address some needs, but the introductory API above deliberately uses only Flask and Python.

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.