So far in this series we have been reading data. Path parameters and query parameters both carry small bits of information tucked inside the URL. That is great for fetching a blog or filtering a list. But what about creating a blog?
Think about it. A blog has a title, some content, an is_active flag, and more. Are we really going to cram all of that into the URL like /blog?title=...&content=...&is_active=true? That is ugly, it breaks on long content, and nobody wants to read that URL. š
This is where the request body comes in. When a client (a browser, a mobile app, anyone) wants to send structured data to our API, it puts that data in the body of the request, not the URL. And the cleanest way to describe that body in FastAPI is with Pydantic.
from fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class Blog(BaseModel): title: str content: str is_active: bool@app.post("/blog")async def create_blog(blog: Blog): return {"message": "Blog created", "data": blog}Let's review this:
Blogis our Pydantic model, the same idea we covered in the Pydantic article. It describes exactly what a blog looks like.Notice the decorator is
@app.postnow, not@app.get. We are creating something, so POST is the right fit.The function takes
blog: Blog. Because the type is a Pydantic model, FastAPI knows this is the request body, not a query parameter.
Now send a POST request to /blog with this JSON body:
{ "title": "My First Blog", "content": "Hello FastAPI!", "is_active": true}FastAPI reads the body, validates it against the Blog model, and hands you a ready-to-use blog object. If the client forgets title or sends is_active as "yup!" instead of a boolean, FastAPI rejects it with a clean error before your code even runs. All that Pydantic validation we learned about earlier is working for you automatically.
Using the data
Inside the function, blog is a real Python object. You access its fields with a dot, just like any class.
@app.post("/blog")async def create_blog(blog: Blog): return {"title": blog.title, "active": blog.is_active}No parsing, no json.loads, no manual checks. The object is already validated and typed.
Mixing body, path, and query together
Here is the part that feels like magic. You can use a path parameter, a query parameter, and a request body in the same endpoint, and FastAPI sorts out which is which.
from typing import Optionalfrom fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class Blog(BaseModel): title: str content: str is_active: bool@app.put("/blog/{blog_id}")async def update_blog(blog_id: int, blog: Blog, notify: Optional[bool] = None): return {"blog_id": blog_id, "data": blog, "notify": notify}Let's review how FastAPI decides:
blog_idis declared in the path (/blog/{blog_id}), so it is a path parameter.blogis a Pydantic model, so it is the request body.notifyis a simple type (bool) and is not in the path, so it is a query parameter.
One function, three sources of data, zero confusion. FastAPI figures it all out from the types and the path.
Gotcha: GET requests are meant for reading, so by convention they do not carry a request body. If you need to send a body, reach for POST, PUT, or PATCH. That is exactly why our examples switched to @app.post and @app.put. š
ā ļø FastAPI tells body from query by looking at the type, not the name. A Pydantic model becomes the body. A simple type like int, str, or bool becomes a query parameter. So if you meant to receive something in the body but typed it as a plain str, FastAPI will quietly go looking for it in the URL instead. Keep an eye on your types. š¤
