FastAPI: The Web Framework That Feels Like Python

Updated: May 26
Building a web API is straight-forward, but there are plenty of points to make small mistakes. We want to build one that validates input, generates docs, handles async properly, and doesn’t make you fight the framework is not.
FastAPI sits in an interesting place. On the one hand, it is not a minimal library like Flask. But on the other hand, it is also not a batteries-included giant like Django. It is opinionated about the problems that waste time, like input validation, serialisation, documentation, and hands you Python’s type system to solve them.
This is the first of two posts. Here we look at the fundamentals. In the next one, we compose APIs that do real work: authentication, background tasks, database integration, and proper error handling.

The Core Idea is Types
Most web frameworks make you write the same code over and over:
Parse the request body
Check if fields exist
Validate their types
Return a 400 if something is wrong
Document what you just wrote
FastAPI essentially reverses the usual API-building workflow. Instead of writing a route, then separately writing validation rules, then separately writing documentation, you declare what the function expects using normal Python type hints. FastAPI uses those type hints to validate incoming requests, convert data into Python objects, return JSON responses, and generate interactive API documentation automatically. The official FastAPI documentation describes this as one of the main ideas of the framework: declare parameter and body types once, and FastAPI handles validation, conversion, OpenAPI documentation, and interactive docs from that declaration.
Here is the simplest possible API:
Here the line app = FastAPI() creates the FastAPI application object. The decorator @app.get("/") tells the system: if someone sends a get request to the root path, run this function. The function then returns a simple Python dictionary, this will automatically be serialised as JSON. So the client should receive
{
"message": "Hello"
}For this to work we have to start a server, in this case we use the development server to host this locally. You can either start this via
fastapi dev main.pyor you can start the uvicorn web server directly via
uvicorn main:app --reloadUvicorn expects the application path in the form module:object, so main:app means “import app from main.py.”
To access this quickly, open a browser and the address
http://127.0.0.1:8000and you will see
{"message":"Hello"}Or alternatively you can call it directly from another terminal, using
curl http://127.0.0.1:8000Now we have a simplistic type of JSON API hosted locally. You can also access a documentation of your API by opening
http://127.0.0.1:8000/docsin the browser.
Request Models with Pydantic
The real power shows up when you need structured input. Instead of manually parsing JSON. We make another short example, but this time we add a Pydantic Model to it.
In case a request comes here, we will only return if we successfully performed the following:
Validated username length
Checked that email is actually an email
Ensured age is between 18 and 120
Set newsletter to False if not provided
Returned a 422 with detailed errors if anything failed
Added all of this to the auto-generated docs
Now if we, send a bad POST request:
curl -X POST http://127.0.0.1:8000/users/ -H "Content-Type: application/json" -d '{"username": "ab", "email": "not-an-email", "age": 15}'We get back:
{
"detail": [
{
"type":"string_too_short",
"loc":["body","username"],
"msg":"String should have at least 3 characters",
"input":"ab",
"ctx":{"min_length":3}
},
{
"type":"value_error",
"loc":["body","email"],
"msg":"value is not a valid email ...",
"input":"not-an-email",
"ctx":{"reason":"An email address must have an @-sign."}
},
{
"type":"greater_than_equal",
"loc":["body","age"],
"msg":"Input should be greater than or equal to 18",
"input":15,
"ctx":{"ge":18}
}
]
}We did not need to write any of the error handling code and we get a detailed explanation for each location. We see what the input was for each location that had an error, what type of error we have, we get a message explaining the error and the code part that failed to be validated. In the terminal where you have the local web server running, you will also see the error codes for each request.
In addition you can open the local documentation:
http://127.0.0.1:8000/docs#/and you get a documentation in which you can also easily test requests without the need for the terminal.
Parameters: The Right Defaults
FastAPI infers what you want from the function signature. If we look at the following example
Then FastAPI infers the following:
item_id comes from the URL path, for example: /items/16
Everything else comes from query strings: /items/16?q=search&skip=5
This is also fully documented under
http://127.0.0.1:8000/docs#/There you will see a beautiful list of all parameters, including whether they go into the path or are a query. Especially note that type conversion happens automatically and optional parameters work as expected. For example we can easily leave out any of the queries without problem, but if we leave out the path parameter, we get a 404 error. Or if we request /items/not-a-number, we get:
{
"detail": [
{
"type":"int_parsing",
"loc":["path","item_id"],
"msg": "Input should be a valid integer...",
"input":"not-a-number"
}
]
}Depending on your version, this might look slightly different. But the important part is that nothing of this had to be handled by us, it is all provided by FastAPI.
Responses: Control What Goes Out
Of course, input validation is only half the problem. The other half is to make sure that output serialisation happens as we want it. So for this we look at the following:
Imagine that our input is a user using the UserCreate model we used above. For the user in the database we save the username and the email, as usual we only do that if the user parameter was validated successfully. We also add a password and a date at which it is created. As this is only a simulated situation we of course skip the real saving of user_in_db in the database. We also return it, but here is the catch: the response_model is only UserResponse, which is missing the password, so even though we return a UserInDB object with a password hash, FastAPI only serialises the fields in UserResponse. The password never leaves the server.
This is also invaluable for refactoring safety. You can change internal models without breaking the API contract, assuming of course that you are not missing out on anything that the response model needs.
Dependency Injection
Most frameworks make you choose between global state and passing arguments everywhere. FastAPI has a third option: dependencies.
Here is a common pattern, getting the current user from a token:
Now every endpoint that needs the current user can declare
user: dict = Depends(get_current_user)and FastAPI will call get_current_user before the function runs and then passes the result of that to the endpoint. If you would run the above local server and request
curl http://127.0.0.1:8000/meYou will get the expected error message with a 422 error code, since the header is missing. If you instead request
curl -H "Authorization: Bearer wrong_token" / http://127.0.0.1:8000/meYou will get a 401 error, since the token is invalid. If instead you change the token to the correct one, you get the expected answer.
Of course, dependencies can depend on other dependencies. So a construction like this
is perfectly fine. The framework builds the dependency graph and executes it in the right order. You can compose behaviours and it simply works. Of course in a real code you should raise appropriate exceptions to make sure you know what breaks.
Async: Use It When You Need It
FastAPI supports both sync and async handlers. You choose based on what you are doing.
The framework uses different execution models automatically. Here sync functions run in a thread pool, wile async functions run on the event loop. One our end, we only need to change the declaration of the function.
One should note here, that dependencies can cross between sync and async. So for example you can make an async function depend on a sync one. But of course this will easily mess up everything, for example if your async function depends on a sync function that makes the database query, then this will block the event loop.
When NOT to Use FastAPI
FastAPI is excellent for APIs. It is not a full-stack framework.
Skip it if you need:
Server-rendered HTML templates (use Django or Flask with Jinja2)
Built-in admin panels (Django does this better)
Opinionated project structure (Django again)
Minimal dependencies (Flask is lighter)
Also consider:
If your team does not use type hints, most the value disappears
If you are building a GraphQL API, Strawberry or Ariadne are better fits
If you need WebSockets beyond simple cases, Starlette (FastAPI’s foundation) gives you more control
FastAPI shines when:
You are building a JSON API
You want auto-generated docs
You care about input validation
You use Python 3.7+ with type hints
You might need async later
We did not cover here how to make the API available outside your local server. This depends on your architecture and is better done by going into the FastAPI documentation.
What Comes Next
This post covered the foundation: how FastAPI uses types to eliminate boilerplate. In the next post, we build real applications:
Proper authentication with JWT
Background tasks and worker queues
Database integration with SQLAlchemy
Error handling that makes sense
Testing APIs without pain
The idea is: declare what you want, let the framework handle the mechanics. That philosophy extends to everything we cover next.
Further Reading
FastAPI Documentation — Comprehensive and well-organised
Pydantic Docs — Understanding the validation engine
Starlette — The ASGI framework underneath FastAPI

Comments