FastAPI: Returning JSON and HTML depending on Request

Kay Herklotz
4 min readJan 14, 2024

--

Representation of a website and data sheet next to each other.
Serving a HTML site and JSON from the same source (generated by author using ideogram.ai)

It would be nice to see HTML in the browser and have your app receive JSON.

The ability to create versatile and efficient web services is not just an asset but a necessity. Ideally we want to deliver products, which can satisfy the needs for a range of different users. Out products should fit the need of everyday users, but also extend to advanced usage coming from other developers to integrate our service into their app for example. Companies like Stripe built their empire on begin developer friendly. How can we translate such a requirement, when writing a service using FastAPI?

The Goal

We want to write a service, which can serve both HTML and JSON from the same endpoint. This way our service should serve a dual purpose, where one the one hand it seamlessly integrates with other services and on the other hand it becomes easily accessible to non-technical stakeholders.

The Solution

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.get("/data", response_class=HTMLResponse)
async def get_data(request: Request):
data = {"message": "Hello, World"}

# Check the accept header in the request
if "application/json" in request.headers.get("accept", ""):
return JSONResponse(data)

# Default to HTML response
return templates.TemplateResponse("data.html", {"request": request, "data": data})

Create an HTML template (data.html) in the templates directory. This template will be used to render the HTML response.

<!DOCTYPE html>
<html>
<head>
<title>Data Page</title>
</head>
<body>
<h1>{{ data.message }}</h1>
</body>
</html>

Run the app with

uvicorn main:app --reload

You can test the endpoint using different Accept headers:

  • For JSON: curl -H "Accept: application/json" http://localhost:8000/data
  • For HTML: Simply navigate to http://localhost:8000/data in a web browser.

I have also created a Github gist for the code here.

Is This Approach RESTful?

Simply creating a service operating over HTTP does not automatically make it RESTful. One may argue that the given URL is now ambiguous, since it returns two different formats of data. Thus to make it RESTful the URL should explicitly specify, if a HTTP resource is requested.

I would argue differently at this point, since I believe that this approach follows REST’s principles. For that I would like to go over the five main principles and show that each is fulfilled:

  1. Uniform Interface: RESTful APIs are designed to have a uniform interface, which simplifies and decouples the architecture. Our FastAPI endpoint uses standard HTTP methods and headers (like the Accept header) in a conventional manner. In REST, each resource is uniquely identified by a URI. In the FastAPI example, the /data endpoint represents a specific resource. In our example the resource is the same, we are not adding or removing data for each representation. REST allows resources to have multiple representations (like JSON, HTML, XML, etc.). The FastAPI endpoint exemplifies this by returning either JSON or HTML based on the Accept header, which is a core RESTful practice known as content negotiation.
  2. Stateless Communication: Each request from client to server must contain all the information needed to understand and process the request. FastAPI handles this inherently, and in our example, the endpoint determines the response format based on the Accept header sent with each request, maintaining statelessness.
  3. Client-Server Architecture: The client and the server act independently, and the client doesn’t need to know anything about the business logic. In the FastAPI example, the client only needs to know the endpoint and the type of response it prefers (JSON or HTML), adhering to this separation.
  4. Cacheable: Taking the information of the Accept header into consideration, both the data and header information can be stored in a cache to quickly respond to requests of the same nature.
  5. Layered System: REST allows for a layered system architecture where client cannot ordinarily tell whether it is connected directly to the end server or to an intermediary along the way. While not explicit in the FastAPI code snippet, this principle is generally supported by FastAPI’s design and is more related to how you deploy your application.

Conclusion

By implementing a single endpoint that intelligently serves both JSON and HTML content, we’ve unlocked a new level of versatility and practicality in our web services. This approach both enhances the user and developer experience, catering to both machine and human interactions seamlessly.

Is this approach something you would implement in your next project? Would you like to learn always something new in the field of software engineering? Join me on this journey as I explore topics revolving around AI, Technology, and Software Engineering.

--

--