CMS for FastAPI: Add Content to an API-Only Stack
Add a blog to an API-first stack, the async way
FastAPI is built for APIs, not content. It's async, typed, and fast — perfect for backends and microservices. But plenty of FastAPI projects also need a public-facing content layer: a blog for SEO, a docs section, marketing pages. FastAPI gives you none of that out of the box, and it's not meant to. The clean fit is a headless CMS that stores content and serves it over an API your FastAPI app (or your front end) consumes. This post covers how to wire it up and why a self-hosted content layer suits an async Python stack.
Where content fits in a FastAPI project
FastAPI usually powers a JSON API consumed by a separate front end (React, Vue, mobile). Content fits one of two ways:
Front end fetches the CMS directly. Your React/Vue app calls the headless CMS API for blog/marketing content, and FastAPI stays purely your application API. Cleanest separation.
FastAPI proxies or aggregates. FastAPI fetches CMS content server-side and includes it in responses — useful if you want a single API surface or need to combine CMS data with app data.
Either way, the CMS is a content source with its own API, and FastAPI never becomes a CMS.
Fetching CMS content in FastAPI (async)
FastAPI is async, so use an async HTTP client like httpx:
import httpx
from fastapi import FastAPI, HTTPException
app = FastAPI()
CMS = "https://cms.yoursite.com/api/v1"
@app.get("/content/posts")
async def list_posts():
async with httpx.AsyncClient() as client:
resp = await client.get(f"{CMS}/posts", params={"per_page": 20})
return resp.json()["data"]
@app.get("/content/posts/{slug}")
async def get_post(slug: str):
async with httpx.AsyncClient() as client:
resp = await client.get(f"{CMS}/posts/{slug}")
if resp.status_code == 404:
raise HTTPException(status_code=404, detail="Post not found")
return resp.json()["data"]
Cache responses (e.g. with an in-memory or Redis layer) so you're not calling the CMS on every request. The CMS returns a { success, message, data } envelope, and drafts return 404.
Why self-hosted headless fits a FastAPI stack
FastAPI teams value performance, control, and running their own infrastructure. A SaaS CMS with metered API calls and vendor-owned data cuts against that. A self-hosted CMS with a plain REST API fits the async, self-owned model.
UnfoldCMS is one option. It's a self-hosted CMS on Laravel with a REST API at /api/v1/*. It's PHP, not Python — but headless means the CMS is an HTTP service, so your FastAPI code only sees JSON. Run it on your own server alongside your stack.
Key fits:
- REST + JSON — works cleanly with
httpxand async handlers. No GraphQL client needed. - No SDK — nothing added to your dependencies. Standard async HTTP.
- Self-hosted, pay once — no per-request billing, content in your own database.
Cache invalidation with webhooks
If you cache CMS content (you should), clear it when content changes. UnfoldCMS fires outgoing, HMAC-signed webhooks on publish/update/delete. Point one at a FastAPI endpoint:
@app.post("/webhooks/cms")
async def cms_webhook(request: Request):
if not verify_hmac(request): # check the signature header
raise HTTPException(status_code=403)
await cache.clear("posts:*") # bust cached content
return {"ok": True}
An editor publishes, the CMS pings FastAPI, the stale cache clears immediately. The HMAC signature ensures only your CMS can trigger it.
Tradeoffs to weigh
- A network dependency. Content comes from an API call. Caching hides the cost, but it's a moving part.
- A second runtime. UnfoldCMS is a PHP app you host. You don't write PHP, but you operate it. If your team runs only Python, that's a genuine objection.
- No revision history. UnfoldCMS doesn't keep past versions of posts.
- Preview needs wiring. Drafts 404 on the public API, so previewing takes a token-authed route.
- REST only, no GraphQL.
FAQ
Can FastAPI serve a blog or marketing content? Not on its own — FastAPI is an API framework with no CMS or content model. Connect a headless CMS and fetch its content via HTTP, or let your front end call the CMS directly.
How do I fetch CMS content asynchronously?
Use an async HTTP client like httpx inside your FastAPI handlers to call the CMS REST API. Cache responses to avoid per-request calls.
Can a Python async app use a PHP-based CMS?
Yes. In a headless setup the CMS is an HTTP service. FastAPI calls it with httpx and never touches PHP — the CMS's language doesn't matter.
Should my front end call the CMS directly or go through FastAPI? Direct is cleanest if the content is public and separate from app data. Proxy through FastAPI when you need one API surface or must combine CMS content with application data.
Bottom line
FastAPI builds APIs, not content — so for a blog, docs, or marketing pages, connect a self-hosted headless CMS. Fetch its content with an async client like httpx, cache it, and bust the cache with a webhook on publish. It's a second runtime to operate and PHP under the hood, but your FastAPI code only ever sees JSON, and you keep the performance and control the stack is built for.
See the API in the demo or read what is a content API.
Related: CMS for Django · CMS for Flask · What is a content API
Free & Open Source
Own your CMS. No subscriptions.
Unfold CMS is free to download and self-host. Built on Laravel + React, full source code included.
Share this post: