Flask JSON APIs: Request Bodies, jsonify, and Status Codes

• Python • By TryzTech Team
FlaskPythonBackendAPIWeb Development

Flask can return HTML pages, but it also works well as an API for JavaScript frontends, mobile applications, or other services. An API client does not need a layout. It needs data with a stable shape, meaningful status codes, and errors it can process without guessing the contents of a string.

We will build create and detail endpoints for notes. Both use the database-backed Note model and wrap successful results in a data key.

Keep Response Shapes Consistent

Choose a pattern before the number of endpoints grows. A successful response in this example looks like this:

{
  "data": {
    "id": 1,
    "title": "Learn the Flask API"
  }
}

A failed response uses an error object:

{
  "error": {
    "code": "validation_error",
    "message": "The title field is required."
  }
}

The key names do not have to match this example exactly. Consistency matters more than finding one supposedly perfect format.

Create the Note Endpoints

# File: routes/api.py
from flask import Blueprint, jsonify, request
from models import Note, db

api_bp = Blueprint("api", __name__, url_prefix="/api")


def note_data(note):
    return {"id": note.id, "title": note.title}


@api_bp.post("/notes")
def create_note():
    if not request.is_json:
        return jsonify({
            "error": {
                "code": "unsupported_media_type",
                "message": "Use Content-Type application/json.",
            }
        }), 415

    payload = request.get_json(silent=True)
    if not isinstance(payload, dict):
        return jsonify({
            "error": {
                "code": "invalid_json",
                "message": "The JSON body must be an object.",
            }
        }), 400

    title = payload.get("title")
    if not isinstance(title, str) or not title.strip():
        return jsonify({
            "error": {
                "code": "validation_error",
                "message": "The title field must be non-empty text.",
            }
        }), 422

    title = title.strip()
    note = Note(title=title)
    db.session.add(note)
    db.session.commit()

    return jsonify({"data": note_data(note)}), 201


@api_bp.get("/notes/<int:note_id>")
def get_note(note_id):
    note = db.session.get(Note, note_id)
    if note is None:
        return jsonify({
            "error": {
                "code": "not_found",
                "message": "The note was not found.",
            }
        }), 404

    return jsonify({"data": note_data(note)})

The note_data() helper converts a model object into a dictionary that is safe to return. Avoid serializing a model’s entire __dict__; it may contain SQLAlchemy internals or sensitive fields that should never leave the API.

Check Content Type Before Reading the Body

A client sending JSON should include the Content-Type: application/json header. request.is_json checks that media type. If it is wrong, the endpoint returns 415 Unsupported Media Type and explains the expected format.

Next, request.get_json(silent=True) attempts to parse the body. With silent=True, malformed JSON produces None, allowing the endpoint to construct its own error. The isinstance(payload, dict) check also rejects arrays and other JSON values because this endpoint expects an object containing title.

Before calling .strip(), the code confirms that title is a string. JSON can also carry numbers, booleans, arrays, and objects. Without this type check, a payload such as {"title": 123} would produce an internal error when the number is treated like text.

Why Are the Status Codes Different?

  • 201 Created means a new note was stored successfully.
  • 400 Bad Request means the JSON body could not be read in the expected shape.
  • 404 Not Found means the URL format is valid, but no note exists with that ID.
  • 415 Unsupported Media Type means the client sent an unsupported body format.
  • 422 Unprocessable Content means the JSON was parsed, but its field values failed validation.

Status codes let a client choose an action before reading the detailed message. A frontend can mark a field after 422, render a missing page after 404, or fix its request header after 415.

Test the API with curl

Create a valid note:

# File: terminal
curl -i -X POST "http://127.0.0.1:5000/api/notes" \
  -H "Content-Type: application/json" \
  -d '{"title":"Learn the Flask API"}'

Then request the newly created note:

# File: terminal
curl -i "http://127.0.0.1:5000/api/notes/1"

The -i option includes response headers so the status code and Content-Type are visible. Repeat the request without the header, with {}, with a numeric title, and with an ID that does not exist.

Do Not Mix HTML and JSON Errors

If an API route fails and Flask returns an HTML error page, a JSON client may be unable to process it. Add Blueprint-level or application-level error handlers so errors intended for /api keep the same JSON format.

After create and detail work, add list, update, and delete endpoints with the same response pattern. Once clients can predict both successful and failed shapes, the API remains comfortable to use even as the endpoint count grows.

Keep reading within the same topic.

Don't Miss Out

Get the latest tech articles, tips, and insights delivered to your inbox.