What FastAPI Is

A thin layer over two ideas - ASGI and Pydantic - and almost nothing that looks like magic is anything else.

Overview

Two libraries wearing one name

FastAPI sits between two libraries that do most of the work. Pydantic handles data validation. Starlette handles HTTP routing, requests, and responses. FastAPI's own contribution is the glue: it reads your function signatures, works out where each parameter comes from, and generates API documentation from the same type annotations.

That separation matters when you debug. A 422 validation error is a Pydantic question. A route that does not match is a Starlette question. FastAPI itself is responsible for wiring parameters to the right source. Knowing these boundaries saves you a lot of time.

This article starts with the two libraries underneath and what each one provides. It then looks at the ASGI interface that makes a FastAPI app a single async function. From there it covers how annotations become contracts, how validation runs before your handler, and what the framework generates without any extra code. The final sections address common misconceptions and where to look when something breaks.

A great deal of what people call "FastAPI magic" isn't FastAPI at all.

Pydantic does the check. FastAPI has no check code of its own. It reads your annotations, hands the request body to a model. Converts the resulting ValidationError into a 422 response. each coercion rule, each error type, each constraint you can write is Pydantic's.

Starlette does the HTTP. Routing, requests, responses, middleware, background tasks and the ASGI plumbing are Starlette's. This FastAPI builds on rather than replaces.

What FastAPI itself adds is the join: reading a function's signature to work out which params come from the path, the query string, the body, a header or a dependency. And making an OpenAPI doc from the same data.

That is a small amount of code doing a thing valuable. Knowing where the seams are makes both debugging and reading the source much easier. A 422 you disagree with is a Pydantic question. A route that doesn't match is a Starlette question. A param arriving from the wrong place is a FastAPI question.

Worth knowing

FastAPI has no validation layer of its own. It hands request data to Pydantic and turns the resulting ValidationError into a 422.
Underneath, an app is one async function taking (scope, receive, send). That interface is ASGI, and uvicorn exists to translate sockets into it.
Because the interface is a plain function call, an app can be exercised without a network — which is what the editors on this page do.
The interactive docs are generated from model_json_schema() for every model you used. Writing a description is writing documentation.
Validation happens before your handler. A 422 means the function never ran.
Both def and async def endpoints are supported, and the choice has real consequences — covered in the runtime tier.

What FastAPI Is, and What It Is Not

A thin layer over two ideas you can learn apart - and almost nothing that looks like magic is anything else.

Four lines is a working API

A function, a decorator and a type annotation. Everything else on this page is an elaboration of these.

example_01.pyFastAPI
Output

The annotation does the work

The same argument, twice: once as a plain string, once annotated. Only one of them is checked and converted.

example_02.pyFastAPI
Output

Underneath it is one async function

An ASGI app takes a request as a dictionary and sends the response back as messages. uvicorn's whole job is translating sockets into that.

example_03.pyFastAPI
Output

The docs are generated, not written

The annotations become a JSON Schema, which becomes an OpenAPI document, which becomes the page at /docs.

example_04.pyFastAPI
Output

Errors before your code runs

A request that does not fit never reaches the handler. The 422 is produced by validation, not by anything you wrote.

example_05.pyFastAPI
Output

Sync and async both work

A def endpoint and an async def one are both ordinary here. Which to use is a real decision, and gets its own module.

example_06.pyFastAPI
Output

Underneath: one async function

An ASGI use is a callable:

async def app(scope, receive, send):
    ...

scope is a dict showing the request. receive is an awaitable that returns incoming messages. send is an awaitable that takes outgoing ones. That is the entire interface. A FastAPI app meets it.

uvicorn's job is to accept a TCP link, parse HTTP, build that scope, call your app. Write the response messages back to the socket. It's a translator between sockets and function calls.

The third editor above does the translation by hand. It builds a scope, calls the app. Prints the messages that come back. Two of them. http.response.start with the status and headers, http.response.body with the content. And that is a full HTTP response.

This matters in practice. Because the interface is a plain function call, you can exercise an entire use without a network. This Is what makes fast tests possible and what makes the editors on this page work at all.

The annotation is the contract

@app.get("/modules/{module_id}")
def read(module_id: int):
    ...

The path says there's a module_id in the URL. The annotation says it's an int. From those two facts FastAPI extracts the value, asks Pydantic to convert it, rejects the request with a 422 if it can't. Records in the OpenAPI doc that this endpoint takes an int path param.

Drop the annotation and you get a string, unvalidated and undocumented. The second editor above shows both side by side: /untyped/42 gives you "42" and /typed/42 gives you 42.

That single gap is most of the value proposition. You were going to write the annotation anyway. The framework decided to act on it.

Validation happens first

A request that doesn't fit never reaches your function.

This is worth stating plainly because it changes how handlers are written. There's no defensive checking at the top of the function, no if not isinstance(...), no try. Int(...). By the time your code runs, each param is the type you declared and each constraint has passed.

The fifth editor above shows it by recording which requests reached the handler: the invalid one is simply absent.

What you get without asking

Because the annotations show the data just, a few things follow for free.

Interactive docs at /docs, made from the OpenAPI schema, with the descriptions and examples you wrote on your fields.

A machine-readable contract at /openapi.json, from which clients can be made in a dozen languages.

Editor support, because your handler's params have real types.

None of that is a split feature you enable. It's a result of the schema. This Is a result of the annotations.

What it is not

It isn't async-only. A def endpoint is fully supported and is often the right choice. The framework runs it in a threadpool so it can't block the event loop.

It isn't a full-stack framework. There's no ORM, no admin, no template rule, no migrations. Django gives you all of that. FastAPI gives you an API layer and leaves the rest to you. That is a trade, not a ranking.

It isn't fast because of its own code. The speed comes from Starlette's ASGI design and from Pydantic v2's Rust core. FastAPI's part isn't getting in the way.

It isn't a swap for understanding HTTP. Status codes, methods, headers and caching are still yours to get right. The framework will happily let you return 200 for a failure.

A note on these pages

There's no server here, and there can't be: a browser tab can't listen on a port.

What runs instead is the app itself, called through ASGI by a client defined before your code. That is the same interface uvicorn uses. Routing, check, status codes, dependencies and the made schema all behave just as they do in production. Because they're the same code paths, with the network removed.

Two things truly differ, and the modules that touch them say so. Streaming responses and middleware need a real event loop. And WebSockets need a real link. This No amount of cleverness will conjure in a sandbox.

all else on this track is the real thing.

How a request actually travels

Tracing one request end to end makes the layers concrete.

A client opens a TCP link and sends bytes. uvicorn parses them into an HTTP request and builds a scope dict: method, path, query string, headers, client address.

It calls your app with (scope, receive, send). Starlette's router walks its route table in registration order, checking the path and method. Finds a match. Extracting any path params as strings along the way.

FastAPI's dependency detail then runs. It works out, from the handler's signature. Here Each param comes from. This one from the path, that one from the query string, this one is a body, that one is a dependency to call first. Anything needing conversion goes to Pydantic. A failure here turns a 422 without your function ever being called.

Your handler runs, with real Python objects as args.

The return value goes through the response model if you declared one, then is serialised to JSON. Starlette sends http.response.start followed by http.response.body. uvicorn turns those back into bytes on the socket.

each step in that chain is plain Python. Nothing is hidden. Each layer can be exercised on its own. Which is why testing a FastAPI app needs no server.

Why it is fast

Three reasons, none of which is FastAPI's own code.

ASGI is asynchronous. A WSGI server handles one request per worker thread while that request waits on a DB. An ASGI server can have thousands of requests in flight, each parked at an await. For an API that spends most of its time waiting on I/O. Which is most APIs. That is a large gap in how much hardware you need.

Pydantic v2 validates in Rust. check used to be a measurable fraction of request time in v1. In v2 it often isn't.

Starlette is thin. Its routing and request handling do very little per request.

FastAPI's part isn't adding cost on top. That is a real achievement and it's worth being clear that the framework isn't itself doing anything clever with speed.

The caveat that matters: none of this helps if your handler blocks. An async def endpoint that makes a synchronous DB call stops the entire event loop. One slow query can stall each concurrent request in the process. That trap has a module of its own in the runtime tier. It's the single most common way a fast framework is made slow.

What to read when something goes wrong

Knowing the seams tells you where to look.

A 404 you didn't expect is routing. Check registration order and trailing slashes.

A 422 you disagree with is Pydantic. Look at the model, not the endpoint.

A 500 mentioning output is often the response model, or a type your model doesn't know.

A **param arriving as None** is FastAPI's signature analysis deciding it came from one spot other than you assumed. Often a body treated as a query param.

Nothing happening at all is uvicorn, or a route registered on a router that was never included.

That mapping saves more time than any amount of framework docs. It turns "FastAPI is broken" into a exact question about a exact library.

Mistakes people make

Blocking the event loop. Writing async def and then making a synchronous DB or HTTP call inside it. The whole process stalls. The framework's headline gain is gone. If a function does blocking work, declare it def and let FastAPI put it on a threadpool.

given FastAPI validates. It doesn't. Pydantic does. When a 422 surprises you, the model is the thing to read. Each rule from the Pydantic track applies unchanged.

Fighting the schema. People at times work around the made docs instead of raising the annotations that give it. A vague schema almost always means vague types. A str that should be a Literal, a missing constraint, an absent response model.

Treating it as a full-stack framework. There's no ORM, no admin, no migrations. That is a deliberate scope. Expecting Django's batteries leads to a lot of disappointed searching.

Putting business logic in handlers. A handler should translate between HTTP and your use. Logic inside one can't be tested without building a request or reused from a background job.

Skipping the seams. Knowing that routing is Starlette, check is Pydantic and signature analysis is FastAPI turns most debugging from guesswork into a exact question about a exact library.

Where to go next

The next module builds a first endpoint properly and looks at what the decorator really does. After that come the three places data arrives from. The path, the query string and the body. Which between them account for most of what an API takes in.

Why the seams are worth knowing

Most frameworks ask you to learn *the framework*. FastAPI is unusual in that most of what you learn transfers.

Pydantic is used far beyond web APIs. Config, LLM structured output, data pipelines, CLI args. all from the Pydantic track applies here unchanged. All you learn here about models applies back there.

ASGI is a standard, not a FastAPI invention. Starlette, Django's async stack, Litestar and Quart all speak it. An ASGI middleware written for one works with the others.

So the framework-exact surface is truly small: the decorators, the way a signature is analysed. Depends. all else is two libraries and a protocol you'd gain from knowing even so.

That is the arg for learning the layers rather than the recipes. A tutorial teaches you what to type. Knowing which library owns which action tells you what to do when the typing doesn't work.

In one paragraph

FastAPI reads the annotations you were going to write anyway, uses Pydantic to enforce them, uses Starlette to move the bytes. Makes an OpenAPI doc from the same data. The layer it adds is thin and mostly consists of working out where each of your function's params should come from. Learn the two libraries underneath and the framework itself takes an afternoon.

Questions people ask

Is FastAPI a web server? No. FastAPI is a framework that defines your routes and validation; a server such as Uvicorn runs it and handles the network connections.

Is FastAPI built on Flask? No. It is built on Starlette for the web layer and Pydantic for data validation, and it does not depend on Flask.

Do I have to use async def? No. Plain def endpoints work and run in a thread pool, so they do not block the event loop.

Why does FastAPI need type hints? The hints are the contract. FastAPI reads them to decide where each value comes from, how to convert it, and what to show in the docs.

Where do the interactive docs come from? FastAPI builds an OpenAPI schema from your routes and type hints, then serves Swagger UI at /docs and ReDoc at /redoc automatically.

Is FastAPI only for small APIs? No. Routers, dependencies, and settings let it scale to large codebases, and many production services use it.

A closing thought

The most handy thing to carry out of this module isn't a fact about FastAPI but a habit of asking which layer you're in.

Routing, check, signature analysis, output and transport are five other concerns owned by three other libraries. Nearly each confusing action turns obvious once you know which one is in charge. Nearly each search turns productive once you search for that library instead of for the framework.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Which library does FastAPI use for validation?

  2. What is an ASGI application?

  3. A request fails validation. Does your handler run?

  4. Where does the interactive documentation come from?

Cheat sheet

What FastAPI Is

See what FastAPI actually is: a thin layer over Starlette and Pydantic. Learn the two ideas that explain almost every feature it offers.

FASTAPI · vizlearn.in/fastapi/what_is_fastapi.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.