FastAPI
ASGI, Pydantic validation, dependency injection, response models, async endpoints and JWT auth.
15 questions
JuniorTheoryVery commonWhat does Depends() do in a FastAPI endpoint signature?
What does Depends() do in a FastAPI endpoint signature?
It declares that FastAPI must call that callable before the endpoint and pass its return value in. The dependency's own parameters are resolved the same way, so it can have sub-dependencies, and its result is cached for the rest of that single request.
Common mistakes
- ✗Believing you must call the dependency yourself inside the handler
- ✗Expecting the same dependency to run once per parameter rather than once per request
- ✗Forgetting a dependency can declare its own parameters and sub-dependencies
Follow-up questions
- →How do you force a dependency to run again within the same request?
- →Where do you attach a dependency that must guard every route in a router?
JuniorTheoryVery commonWhat is FastAPI built on, and where does its speed actually come from?
What is FastAPI built on, and where does its speed actually come from?
It is a thin layer over two libraries — Starlette for ASGI routing and Pydantic for validation — run by an ASGI server such as uvicorn. The speed comes from async I/O concurrency plus Pydantic's compiled Rust core, not from Python itself.
Common mistakes
- ✗Calling FastAPI a full-stack framework — it has no ORM, admin or migrations
- ✗Believing the speed comes from FastAPI's own code rather than ASGI plus Pydantic's Rust core
- ✗Forgetting that an ASGI server such as
uvicornruns the app — FastAPI is not a server
Follow-up questions
- →What does
Starlettecontribute thatPydanticdoes not? - →Why can a single
uvicornworker serve many concurrent requests?
JuniorTheoryVery commonWhat does FastAPI do with a validation-library Pydantic model declared as the body?
What does FastAPI do with a validation-library Pydantic model declared as the body?
It parses the JSON body, coerces and validates every field against the declared types, then passes a typed instance to the handler. On a mismatch the handler never runs and FastAPI returns 422 with a per-field error list. The same model generates the OpenAPI schema.
Common mistakes
- ✗Thinking the annotations are documentation only and are not enforced at runtime
- ✗Expecting a validation failure to surface as a
500rather than a422 - ✗Forgetting the handler receives a model instance, not the raw
dict
Follow-up questions
- →What does the
locfield in a422error body point at? - →How do you reject extra keys that the model does not declare?
JuniorTheoryVery commonHow does FastAPI differ from Django in what the framework itself gives you?
How does FastAPI differ from Django in what the framework itself gives you?
Django is batteries-included — ORM, migrations, admin, auth and templates ship with it on a synchronous core. FastAPI ships only routing, validation, dependency injection and OpenAPI on an async core; the ORM, migrations and admin are yours to choose.
Common mistakes
- ✗Expecting an ORM, admin or migration tool to come with FastAPI
- ✗Thinking Django cannot do async at all — it supports ASGI, but its ORM core stays synchronous
- ✗Choosing between them on syntax instead of on what the project actually needs
Follow-up questions
- →Which parts of Django stay synchronous even under an ASGI server?
- →What would you have to add to FastAPI to replace Django's admin?
MiddleTheoryVery commonHow does FastAPI run an async def endpoint versus a plain def one?
How does FastAPI run an async def endpoint versus a plain def one?
An async def endpoint is awaited directly on the event loop, so a blocking call inside it stalls every other request in that worker. A plain def endpoint is offloaded to a bounded threadpool, where blocking is safe but concurrency is capped by the thread count.
Common mistakes
- ✗Marking an endpoint
async defand then calling a synchronous client inside it - ✗Assuming a plain
defendpoint has no concurrency limit - ✗Thinking
asyncalone makes a blocking library non-blocking
Follow-up questions
- →What happens when the threadpool for plain
defendpoints is exhausted? - →How do you run one blocking call from inside an
async defendpoint?
JuniorTheoryCommonHow does FastAPI decide whether a parameter is a path, query or body value?
How does FastAPI decide whether a parameter is a path, query or body value?
By name and type. If the name appears in the route path it is a path parameter; otherwise a scalar type (int, str, bool, UUID) becomes a query parameter and a Pydantic model becomes the request body. Path(), Query() and Body() override the guess.
Common mistakes
- ✗Assuming argument order in the signature decides the source
- ✗Expecting a single scalar to land in the body without
Body(embed=True) - ✗Forgetting that a name matching the route path always wins over any other source
Follow-up questions
- →How do you force a single scalar argument into the JSON body?
- →What happens when two parameters declare the same query name?
MiddleTheoryCommonWhen is the in-process helper BackgroundTasks the wrong tool for deferred work?
When is the in-process helper BackgroundTasks the wrong tool for deferred work?
It runs the callable in the same process right after the response, with no persistence, no retries and no queue. So it is wrong whenever the work is heavy or CPU-bound, must survive a restart, or needs retrying — a broker-backed queue such as Celery or ARQ owns those cases.
Common mistakes
- ✗Assuming a background task survives a redeploy or a crash
- ✗Putting CPU-bound work there and starving the worker it runs in
- ✗Expecting automatic retries when the task raises
Follow-up questions
- →Why is a resource from a
yielddependency already closed inside a background task? - →What would you keep in
BackgroundTaskseven after adopting Celery?
MiddleTheoryCommonHow does a signed-token JWT bearer dependency protect a FastAPI route?
How does a signed-token JWT bearer dependency protect a FastAPI route?
OAuth2PasswordBearer pulls the token out of the Authorization: Bearer header and raises 401 when it is absent. Your own dependency then verifies the signature and exp, loads the user and raises 401 on failure. Declaring it with Depends both enforces and documents the guard.
Common mistakes
- ✗Decoding the token payload without verifying its signature
- ✗Forgetting to check
exp, so an expired token keeps working - ✗Believing a stateless
JWTcan be revoked without a denylist or a short lifetime
Follow-up questions
- →How would you revoke a token before its
exppasses? - →Where do you attach the guard so it covers every route in a router?
MiddleTheoryCommonWhat does response_model change about what the client actually receives?
What does response_model change about what the client actually receives?
The returned object is validated and re-serialized through that model, so any field the model does not declare is dropped before the response is sent. That is what keeps a password or internal id from leaking, and it also drives the documented response schema.
Common mistakes
- ✗Treating
response_modelas documentation and returning objects that still carry secret fields - ✗Expecting extra fields to pass through instead of being dropped
- ✗Forgetting the response is re-validated, so a bad value fails after the handler already ran
Follow-up questions
- →What status code does a response failing its own
response_modelproduce? - →When would
response_model_exclude_unsetchange the payload?
MiddleTheoryCommonWhy does a badly typed request body return 422 rather than 500?
Why does a badly typed request body return 422 rather than 500?
Validation runs before the handler, so nothing on the server failed — the client's payload is wrong. FastAPI catches RequestValidationError in a built-in handler and answers 422 with a loc/msg/type entry per bad field. Validating by hand inside the handler gives a 500 instead.
Common mistakes
- ✗Assuming
422is a generic error code rather than a validation-specific one - ✗Expecting a
Pydanticmodel built by hand inside the handler to also produce422 - ✗Thinking validation runs inside the handler rather than before it
Follow-up questions
- →How do you replace the default
422body with your own error format? - →Which exception would you handle to log every rejected payload?
MiddleCodeOccasionalReusable pagination dependency with validated bounds
Reusable pagination dependency with validated bounds
Declare limit and offset as ordinary parameters of a plain function with Query(ge=..., le=...) bounds and return them. FastAPI resolves a dependency's own parameters exactly as it resolves an endpoint's, so the bounds are enforced before the handler runs and a bad value yields 422.
Common mistakes
- ✗Reading the query string manually instead of declaring parameters on the dependency
- ✗Clamping out-of-range values silently instead of letting
422reject them - ✗Believing
geandleonQuery()are documentation only
Follow-up questions
- →How would you share this dependency across every route in a router?
- →What changes if two endpoints need different
limitceilings?
MiddleDesignOccasionalYour team is starting a new service that fans out to four third-party HTTP APIs per request and aggregates their responses; each upstream call takes 200-400 ms and the service must hold several thousand concurrent connections on modest hardware. It also needs an internal back-office screen where support staff inspect and correct records, and the data model will change often over the first year. Argue for FastAPI or for Django plus DRF as the stack. Cover which concurrency model fits the upstream fan-out and why, what each option costs you for the back-office screen and for schema migrations, and what you would have to build yourself under the option you chose.
Your team is starting a new service that fans out to four third-party HTTP APIs per request and aggregates their responses; each upstream call takes 200-400 ms and the service must hold several thousand concurrent connections on modest hardware. It also needs an internal back-office screen where support staff inspect and correct records, and the data model will change often over the first year. Argue for FastAPI or for Django plus DRF as the stack. Cover which concurrency model fits the upstream fan-out and why, what each option costs you for the back-office screen and for schema migrations, and what you would have to build yourself under the option you chose.
FastAPI: the work is I/O-bound fan-out, and one async worker can hold thousands of waiting upstream calls that Django's synchronous core would need a thread or process each for. The cost is that the admin screen and migrations are not included — you add SQLAlchemy with Alembic and build or buy the back-office yourself, a bounded one-time cost against a permanent throughput win.
Common mistakes
- ✗Calling an upstream-heavy fan-out CPU-bound and sizing workers by core count
- ✗Assuming Django's ORM is async under ASGI and needs no thread per query
- ✗Ignoring that the admin screen and migrations are real work you take on with FastAPI
Follow-up questions
- →What breaks first if you keep Django and simply raise the worker count?
- →Which parts of the back-office screen would you refuse to build by hand?
SeniorDebuggingOccasionalOne slow endpoint stalls every concurrent request — diagnose it
One slow endpoint stalls every concurrent request — diagnose it
requests is a synchronous client, so the call blocks the worker's single event loop for the whole upstream round trip and /health cannot be served meanwhile. The CPU-bound render_pdf then fights for the interpreter in the same process after the response. Fix: an async client for the call, and a broker-backed queue for the render.
Common mistakes
- ✗Assuming FastAPI offloads a synchronous client automatically inside
async def - ✗Reading rising latency on unrelated routes as a networking problem rather than a blocked loop
- ✗Believing background tasks run in a separate process, so CPU-bound work is safe there
Follow-up questions
- →Why does adding worker processes hide the symptom without fixing the cause?
- →What would you measure to prove the event loop, not the upstream, is the bottleneck?
SeniorTheoryOccasionalWhen does a yield dependency's teardown run relative to the response?
When does a yield dependency's teardown run relative to the response?
After the response has been sent, and before any background task. So you cannot raise HTTPException there to change a reply the client already holds, and a session yielded to the handler is already closed inside a background task — that task must open its own.
Common mistakes
- ✗Raising
HTTPExceptionafteryieldand expecting the client to see it - ✗Using a session yielded by a dependency inside a background task
- ✗Assuming teardown runs before the response is written
Follow-up questions
- →How would you keep a session open for deferred work without leaking it?
- →What happens to the teardown when the handler itself raises?
SeniorPerformanceOccasionalWhat does response_model cost on a hot list endpoint returning 5000 rows?
What does response_model cost on a hot list endpoint returning 5000 rows?
Every row is validated and re-serialized a second time — the ORM objects were already built, and the model rebuilds them field by field before the JSON is written. On large lists that pass dominates the request. Narrow the model, page the result, or return a response object directly and skip the model.
Open full question →Common mistakes
- ✗Assuming
response_modelis a startup-time cost rather than a per-row one - ✗Swapping in a faster JSON encoder while leaving the second validation pass in place
- ✗Serving unbounded lists and treating the result as a serialization problem
Follow-up questions
- →How would you prove the validation pass, not the query, dominates the request?
- →What do you give up by returning a response object directly?