Serving Your First Endpoint
A web framework is a wrapper over one function from request to response; here is that function, returning JSON.
def app(environ, start_response) -> Iterable[bytes] # the WSGI contractUnder Flask, Django and FastAPI there is a single function. It takes a request and returns a response, and everything a framework adds — routing, parsing, validation, sessions — is a way of writing that function more conveniently.
The function underneath
For synchronous frameworks the contract is WSGI: a callable taking (environ, start_response). environ is a dict of CGI-style keys (REQUEST_METHOD, PATH_INFO, QUERY_STRING, wsgi.input for the body). You call start_response(status, headers) with the status as a string — "200 OK", not 200 — and headers as a list of pairs, then return an iterable of bytes objects. Returning a str is a common first mistake. ASGI is the asynchronous equivalent, async def app(scope, receive, send), and is what FastAPI implements.
What the framework does for you
It parses the body, matches the route, coerces types, serialises your return value and sets the headers. In FastAPI, @app.get("/health") is shorthand for app.add_api_route("/health", health, methods=["GET"]); in Flask, @app.route("/health") is documented as a shortcut for app.add_url_rule("/health", "health", health). Same registration, two spellings, and you will meet the long form in code that builds routes in a loop.
Getting JSON right
Three things have to agree: the body is JSON bytes, Content-Type is application/json, and the status matches what happened — 201 with a Location header for a create, 204 and no body for a delete. Without the header a client reads your JSON as plain text.
Example
When to use it
- Adding a /health endpoint a load balancer can poll.
- Exposing an internal script as an HTTP service without learning a framework first.
- Understanding what a WSGI or ASGI error in a traceback is actually telling you.
- Writing middleware, which is a function that wraps the one in this lesson.
More examples
The same endpoint in Flask, both spellings
The decorator and add_url_rule are the same call, and the second form is what you need the moment routes are generated. The endpoint name (the second argument) is what url_for refers to, and it defaults to the function's __name__.
from flask import Flask, jsonify
app = Flask(__name__)
# Shorthand.
@app.route("/health", methods=["GET"])
def health():
return jsonify(status="ok"), 200 # jsonify sets Content-Type for you
# Identical registration, written out. route() is documented as a shortcut for
# add_url_rule() with the view_func argument -- useful when the routes come
# from a list or a plugin rather than from decorators.
def version():
return jsonify(version="1.4.0"), 200
app.add_url_rule("/version", "version", version, methods=["GET"])
# Development only: flask run, or app.run(). In production, a WSGI server:
# gunicorn --workers 4 'myapp:app'FastAPI, which is ASGI
Note that `async def` here is a choice, not a requirement — FastAPI runs a plain `def` endpoint in a thread pool. Pick async only once the handler actually awaits something, which the async stage of this course goes into.
from fastapi import FastAPI, status
app = FastAPI()
# Shorthand: a dict return is serialised to JSON with Content-Type set and 200.
@app.get("/health")
async def health():
return {"status": "ok"}
# Explicit about the status, as a create should be.
@app.post("/orders", status_code=status.HTTP_201_CREATED)
async def create_order(payload: dict):
return {"id": 1041, **payload}
# Longhand, for routes registered dynamically:
# app.add_api_route("/health", health, methods=["GET"])
#
# Run it: uvicorn main:app --port 8000 (main.py holding `app`)Serving the raw WSGI app over a port
Worth running once to see that nothing magic happens between the socket and your function. Bind to 127.0.0.1 rather than the empty string so a development server is not reachable from the rest of the network.
from wsgiref.simple_server import make_server
def app(environ, start_response):
body = b'{"status": "ok"}'
start_response("200 OK", [("Content-Type", "application/json"),
("Content-Length", str(len(body)))])
return [body]
# make_server(host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler)
with make_server("127.0.0.1", 8000, app) as httpd:
print("serving on http://127.0.0.1:8000")
httpd.handle_request() # one request, then exit; serve_forever() to keep going
Discussion