Back to Blog
Python

Python Flask Routes: Handling Requests and JSON Responses

python flask routes requests and json responses: Learn how to build Python Flask routes that accept HTTP requests and return JSON responses, including request parsing,...

FlaskJSONREST APIHTTP RequestsAPI Development
Illustration of a Flask route receiving an HTTP request and returning a JSON response, showing the request-response cycle in a Python web API.

python flask routes requests and json responses requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you build Python Flask routes that handle requests and return JSON responses, the route function's return value becomes the HTTP response body. Flask gives you two ways to produce JSON output: return a Python dictionary directly, or use flask.jsonify().

from flask import Flask, jsonify app = Flask(__name__) @app.route("/health") def health(): return {"status": "ok"}

Since Flask 1.1, returning a dict from a view function automatically serializes it to JSON with the correct Content-Type: application/json header. The jsonify() function does the same thing explicitly and also accepts keyword arguments:

@app.route("/health") def health(): return jsonify(status="ok")

Both approaches produce the same wire format. The difference is mainly readability: jsonify() makes the intent explicit and is useful when the response is built from keyword arguments rather than a dict literal. For a response that already exists as a dict variable, returning the variable directly is shorter and avoids an extra function call.

Reading Request Data in Flask Routes

The request object from Flask gives you access to everything the client sent. The three most common sources of input are query parameters, form data, and a JSON request body.

from flask import request @app.route("/users") def list_users(): page = request.args.get("page", default=1, type=int) limit = request.args.get("limit", default=20, type=int) return {"page": page, "limit": limit, "users": []}

request.args is a MultiDict containing URL query parameters. The get() method with type=int performs conversion and falls back to the default if the value is missing or cannot be parsed.

For a JSON request body, use request.get_json():

@app.route("/users", methods=["POST"]) def create_user(): data = request.get_json() if data is None: return {"error": "Request body must be valid JSON"}, 400 return {"id": 123, "name": data.get("name")}, 201

request.get_json() returns None when the body is empty or the Content-Type header is not application/json. Calling it with silent=True suppresses the 400 error that Flask raises on malformed JSON, which lets you handle the failure yourself instead of returning Flask's default HTML error page.

Choosing the Right HTTP Method for a Route

A route can accept one or more HTTP methods. The methods parameter on @app.route() controls which verbs the route responds to. By default, a route only accepts GET.

@app.route("/items/<int:item_id>", methods=["GET", "PUT", "DELETE"]) def handle_item(item_id): if request.method == "GET": return {"id": item_id, "name": "example"} if request.method == "PUT": data = request.get_json() return {"id": item_id, "updated": data.get("name")} if request.method == "DELETE": return {"deleted": item_id}, 204

When a route accepts multiple methods, you typically branch on request.method inside the function. For larger APIs, flask.views.MethodView gives you a class-based structure where each HTTP method maps to a method on the class. That is cleaner when a single resource has many operations.

Setting Status Codes, Headers, and Content-Type

Returning a tuple of (body, status) is the shortest way to set a status code. The body can be a dict, a string, or a response object.

@app.route("/items/<int:item_id>") def get_item(item_id): if item_id > 100: return {"error": "Item not found"}, 404 return {"id": item_id, "name": "example"}

For more control, build a Response object explicitly:

from flask import Response import json @app.route("/items/<int:item_id>") def get_item(item_id): payload = {"id": item_id, "name": "example"} return Response( json.dumps(payload), status=200, mimetype="application/json" )

Flask sets Content-Type: application/json automatically when you return a dict or use jsonify(). The Accept header from the client is not used to negotiate the response format by default. If you need content negotiation, you must implement it yourself:

@app.route("/items") def items(): if request.accept_mimetypes.best == "text/html": return "<ul><li>example</li></ul>" return {"items": ["example"]}

For most JSON APIs, checking Accept is unnecessary because the client already knows the API returns JSON. But if the same route serves both browsers and API clients, explicit negotiation prevents the wrong content type from reaching the client.

Serializing Python Objects That Are Not JSON-Serializable

Python dicts with string keys and JSON-compatible values serialize without issue. But datetime objects, Decimal values, and custom class instances raise TypeError when Flask tries to serialize them.

from datetime import datetime @app.route("/now") def now(): return {"timestamp": datetime.now()} # TypeError

Flask's JSON encoder does not know how to convert datetime to a string. You have two options: convert the value manually before returning it, or extend the JSON encoder.

from flask.json import JSONEncoder class CustomJSONEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.isoformat() return super().default(obj) app.json_encoder = CustomJSONEncoder

With the custom encoder registered, any datetime in a returned dict is automatically converted to an ISO 8601 string. This keeps serialization logic in one place instead of scattering str() calls across every route.

Handling Errors and Returning JSON Instead of HTML

By default, Flask returns an HTML error page for unhandled exceptions and for HTTP errors raised with abort(). If your API clients expect JSON, register error handlers that return JSON.

from flask import abort @app.errorhandler(404) def not_found(error): return {"error": "Resource not found"}, 404 @app.errorhandler(500) def internal_error(error): return {"error": "Internal server error"}, 500

The error handler receives the exception object as an argument. For HTTPException subclasses, you can read the original status code and description from the exception if you want to echo them in the response body.

from werkzeug.exceptions import HTTPException @app.errorhandler(HTTPException) def handle_http_exception(error): return {"error": error.description}, error.code

This single handler covers all HTTP errors, including 400, 401, 403, 404, and 405. Registering it alongside the 500 handler keeps your API responses consistent even when something fails.

Production Considerations for JSON Routes

The default Flask development server is single-threaded and not designed for production traffic. When you deploy a Flask app that serves JSON responses, run it behind a WSGI server such as Gunicorn or uWSGI, and put a reverse proxy in front of it.

JSON serialization is fast for small payloads, but large response bodies increase memory usage and latency. If a route returns a large collection, consider pagination or streaming the response with Response(..., direct_passthrough=True) when the data source supports incremental generation.

CORS is another concern. If your Flask API is called from a browser on a different origin, the browser blocks the response unless the server sends the appropriate CORS headers. The flask-cors extension handles this cleanly:

from flask_cors import CORS CORS(app)

Without CORS headers, a frontend at http://localhost:3000 will not be able to read a response from a Flask API at http://localhost:5000, even though the request reaches the server. The browser enforces this restriction after the response is received.

Validating Request Data Before Returning JSON

A route that accepts JSON input should validate the data before using it. request.get_json() returns whatever the client sent, including unexpected fields or wrong types. A minimal validation check keeps the route safe:

@app.route("/users", methods=["POST"]) def create_user(): data = request.get_json(silent=True) if not data or "name" not in data: return {"error": "Field 'name' is required"}, 400 if not isinstance(data["name"], str): return {"error": "Field 'name' must be a string"}, 400 return {"id": 123, "name": data["name"]}, 201

For complex schemas, a validation library such as Marshmallow or Pydantic reduces boilerplate. But for a small API, the inline checks above are often sufficient and keep the route readable.

python flask routes requests and json responses: Practical U | RYUSLOG DEV