Cheat SheetsInterview Q&AFastAPI

FastAPI — Cheat Sheet

Interview Q&A · 100 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
FastAPI
Interview Q&A100 topicsQuick revision reference
1

What is FastAPI and what does it build on?

FastAPI is an ASGI web framework built on two things: Starlette for the HTTP layer — routing, middleware, WebSockets, background tasks — and Pydantic for data validation and serialisation. What FastAPI adds on top is the declaration-driven layer. It reads your type hints and Pydantic models and derives from them the request parsing, validation, serialisation, and the OpenAPI schema — so one declaration produces the runtime behaviour and the documentation together. That is the core idea worth stating: the type hints are not documentation, they are the implementation. A parameter annotated int is parsed and validated as an int, and a 422 is returned automatically if it is not. Being ASGI rather than WSGI is the other significant property. It supports async request handlers, WebSockets and long-lived connections, which WSGI frameworks like Flask and Django historically could not. The practical consequence of the layering is that Starlette's documentation is often the right place to look for middleware, static files and testing details, since FastAPI inherits them rather than reimplementing them.

2

How does FastAPI decide where a parameter comes from?

By a set of rules based on the name and the type. If the parameter name matches a placeholder in the path, it is a path parameter. Otherwise, if the type is a scalar — int, str, float, bool, UUID — it is a query parameter. If the type is a Pydantic model, it is read from the request body. So the source is inferred rather than declared, which is what makes simple handlers concise. When you need to override the inference, or add metadata, you use the explicit markers: Path, Query, Body, Header, Cookie, Form and File. Those let you say "this scalar is actually in the body", add validation constraints, set a description for the docs, or give an alias — which is essential for headers, since HTTP header names are not valid Python identifiers. The modern style is Annotated — parameter: Annotated[int, Query(ge=1)] — rather than a default value, because it separates the type information from the actual default and works correctly with reused dependencies. With several body parameters, FastAPI nests them under keys named after the parameters, which surprises people expecting a flat body.

3

Why does route ordering matter?

Routes are matched in the order they are registered, and the first match wins. So a path like /users/{user_id} declared before /users/me will match the literal string "me" as a user_id, and the handler for /users/me is never reached. The failure is confusing because both routes exist and one silently shadows the other — usually surfacing as a validation error when "me" fails to parse as an integer. The rule is to declare more specific paths before more general ones: fixed segments before path parameters. The same applies across routers included in the application — the include order determines matching order. The related consideration is path converters. A path parameter matches a single segment by default, so /files/{path} does not match a nested path. Declaring it as {path:path} matches the remainder including slashes, which is what you need for file-like routes. The practical habit is to keep routes for a resource together and ordered specific-first, and to be alert to it when adding a literal route to an existing resource — that is exactly when the shadowing bug is introduced.

4

What is an APIRouter and how should you structure a large application?

APIRouter is a way to group related routes in a separate module and include them into the application with a prefix, tags, and shared dependencies. It is the mechanism for avoiding a single enormous main module. The structure that works: a router per resource or domain area, in its own module. Each router declares its prefix and tags once at construction, so individual routes do not repeat them. Router-level dependencies apply to every route in the group, which is the right place for authentication on an entire admin section. Beyond routing, the layering that matters is keeping route handlers thin. A handler should parse, delegate to a service or use-case function, and shape the response. Business logic in the handler is untestable without the HTTP layer and cannot be reused. A typical layout is routers, services, repositories or data access, schemas for the Pydantic models, and models for the persistence layer — keeping the API schemas separate from the database models, which matters because coupling them makes every schema change a migration and exposes internal fields. That separation is the single most valuable structural decision.

5

What does response_model do and why should you use it?

response_model declares the schema of the response. FastAPI validates the returned object against it, serialises accordingly, and documents it in OpenAPI. The critical behaviour is filtering. Fields not in the response model are removed from the output, even if the returned object has them. That is a security feature: returning an ORM user object directly would serialise every attribute including the password hash, while a response model with only the intended fields cannot. So it is the mechanism that prevents accidental data exposure, and relying on it is much safer than remembering to construct the right dict by hand. It also gives correct documentation, since the schema is derived from the same declaration. The options worth knowing: response_model_exclude_none omits null fields, which reduces payload size but makes the shape inconsistent. response_model_exclude_unset returns only fields that were explicitly set, which is useful for PATCH responses. In newer FastAPI the return type annotation serves the same purpose, so declaring the handler as returning UserOut is equivalent and reads better. The cost is a serialisation pass, which is measurable on very large responses.

6

How does FastAPI generate OpenAPI documentation?

It introspects the route declarations — path, method, parameters, their types and constraints, the request body model and the response model — and builds an OpenAPI schema from them. Swagger UI and ReDoc render it. The significance is that the documentation cannot drift from the implementation, because it is derived from the same declarations that do the parsing and validation. A schema written by hand alongside the code goes stale within weeks; this one cannot. What you can add: summary and description on the route, or the docstring, which is used as the description. tags for grouping. response descriptions and additional status codes via the responses parameter, which is how you document error responses — and error responses are the part most commonly left undocumented. Examples can be attached to models and parameters, which makes the docs substantially more useful than bare schemas. The practical caveats: the generated schema is only as good as the declarations, so a handler returning a bare dict documents nothing useful. And the interactive docs should usually be disabled or protected in production, since they expose the full API surface.

7

What is the difference between FastAPI and Flask or Django REST Framework?

FastAPI is ASGI-native with first-class async support, and derives validation, serialisation and documentation from type hints. Flask is a minimal WSGI framework — you assemble the rest yourself, choosing an ORM, a validation library, and a documentation approach. That flexibility is its appeal and its cost: two Flask projects can look nothing alike. Async support was added but is not the native model. Django REST Framework sits on Django, so it brings the ORM, admin, migrations, auth and a large ecosystem. It is the most batteries-included, which is a genuine advantage for a conventional CRUD application with a database — you get an admin interface for free, which FastAPI has no equivalent for. The cost is that it is opinionated and heavier, and its serialisers are more verbose than Pydantic models. The honest positioning: FastAPI for APIs, particularly with high concurrency or where the OpenAPI schema matters for consumers; Django for a full application where the admin and ORM integration pay for themselves; Flask when you want minimal structure. Performance differences are real but usually secondary to the database.

8

What is ASGI and why does it matter?

ASGI is the asynchronous successor to WSGI — the interface between a Python web application and its server. WSGI is synchronous and request-response by design: the server calls the application, which returns a response. That model cannot express a long-lived connection, so WSGI has no WebSocket support and no way to stream bidirectionally. ASGI defines an async callable receiving a scope, a receive channel and a send channel. That structure supports HTTP, WebSockets and lifespan events in one protocol, and it allows the server to handle many concurrent connections on a single thread. The practical consequences for FastAPI: async handlers actually run concurrently, WebSockets are supported natively, and startup and shutdown are handled through the lifespan protocol. The deployment implication is that you need an ASGI server — Uvicorn is the usual choice, often run under Gunicorn for process management, or Hypercorn as an alternative. A WSGI server such as plain Gunicorn with sync workers cannot run an ASGI application. And middleware written for WSGI does not apply; ASGI middleware is a different interface.

9

How do you handle application startup and shutdown?

The lifespan context manager, which is the current approach. You write an async context manager that performs startup before yield and shutdown after, and pass it to the FastAPI constructor. That replaces the older on_event("startup") and on_event("shutdown") decorators, which are deprecated. The context manager form is better because setup and teardown for the same resource sit together, so it is harder to add one and forget the other. What belongs there: creating the database connection pool, establishing connections to caches and message brokers, loading a machine learning model, and starting background tasks — and closing all of them on shutdown. What does not belong there: anything slow enough to delay readiness meaningfully, and anything that should fail per-request rather than at startup. The operational points. Failing fast at startup is usually right — a service that cannot reach its database should not report ready. But be careful about hard dependencies that make the service unable to start during a partial outage. And shutdown must be graceful: closing pools and finishing in-flight requests, which is what makes a rolling deployment invisible rather than a burst of errors.

10

How does middleware work in FastAPI?

Middleware wraps the entire request-response cycle. It runs before the route handler, calls the next layer, and can inspect or modify the response afterwards. The common uses: adding a request ID for correlation, timing requests, CORS, GZip compression, and enforcing something globally. The important structural point is that middleware runs outside the routing and dependency system. So it does not have access to the resolved dependencies, and it cannot easily know which route will handle the request. Anything needing that context belongs in a dependency, not middleware. Middleware also runs for every request including unmatched paths, which is sometimes what you want and sometimes surprising. Order matters: middleware is applied in reverse of the order added, so the last added is outermost. That determines whether your timing middleware measures compression, and whether your error handler sees exceptions from other middleware. The performance caution is real. Every middleware adds overhead to every request, and one that reads or buffers the request body can break streaming. A middleware doing per-request I/O — logging to an external service synchronously — is a common cause of unexplained latency.

11

What are background tasks and what are their limits?

BackgroundTasks lets a handler schedule work that runs after the response is sent. You declare it as a parameter and add functions to it. The appeal is returning quickly while doing follow-up work — sending an email, writing an audit record — without making the caller wait. The limits are the important part, and they are significant. The task runs in the same process. If the process is restarted, crashes, or is terminated during a deployment, pending tasks are lost with no record. There is no persistence and no retry. A long-running task keeps the worker busy, so a slow background task reduces the capacity available for requests. A synchronous task function runs in the thread pool, while an async one runs on the event loop — so a blocking async task stalls everything. So BackgroundTasks is appropriate for short, best-effort work where loss is acceptable. Anything that must happen — a payment confirmation, an order fulfilment — needs a real queue: Celery, ARQ, Dramatiq, or a broker with a separate worker. Using it for work that matters is a common and quietly damaging mistake.

12

How do you handle file uploads?

Declare a parameter as UploadFile, which FastAPI populates from a multipart request. It requires python-multipart installed, which is an easy thing to miss and produces a confusing error. UploadFile is preferable to bytes because it uses a spooled temporary file: small files stay in memory, and large ones spill to disk. Declaring the parameter as bytes loads the whole thing into memory, so a large upload is an immediate memory problem and a trivial denial of service. Its methods are async — await file.read() — because the underlying I/O is. The things to handle. Validate the size, since ASGI does not impose a limit by default; checking after reading is too late, so enforce it at the reverse proxy and check content-length as well. Validate the content type, and do not trust the client-supplied one — sniff the actual bytes if it matters. Sanitise the filename, since a client-supplied name containing path separators is a traversal risk. And for anything large, the better architecture is a pre-signed URL so the file goes directly to object storage and never passes through your application.

13

How do you return a streaming response?

StreamingResponse takes an iterator or async generator and sends chunks as they are produced, rather than buffering the whole body. The use cases: large file downloads, generating a CSV export on the fly, and server-sent events for pushing updates to a client. The benefit is memory — a gigabyte export streams with constant memory rather than being assembled first — and time to first byte, since the client starts receiving immediately. The things to be careful about. The generator runs after the handler returns, so anything it depends on must still be alive — a database session closed by a dependency's teardown will already be closed when the generator runs, which is a genuinely confusing failure. The session has to be managed inside the generator, or kept open deliberately. Exceptions raised mid-stream cannot change the status code, because headers are already sent. The client sees a truncated response, so error handling has to be designed for that. And buffering proxies can defeat streaming entirely — nginx buffers by default, so server-sent events need proxy_buffering off. FileResponse is the specialised version for sending a file from disk.

14

How do WebSockets work in FastAPI?

A route decorated with @app.websocket receives a WebSocket object. You accept the connection, then loop receiving and sending messages, handling WebSocketDisconnect when the client goes away. Because the framework is ASGI-native, this is a first-class capability rather than an add-on. The design considerations are where the real work is. Connection state: each connection is a long-lived coroutine holding memory, so the practical limit is much lower than for stateless requests. You need a registry of active connections to broadcast, and it must handle disconnections cleanly or it leaks. Scaling across processes is the hard part. Connections are held by one worker, so broadcasting to all clients requires a shared channel — Redis pub/sub is the usual answer — because a message arriving at one worker must reach clients connected to another. Authentication happens at connection time, since there is no per-message header. Passing a token in the query string is common and leaks it into logs, so a first-message authentication handshake is often better. And load balancers need long idle timeouts, or connections are dropped silently.

15

What does Pydantic actually do for you?

It parses and validates data against a declared model, and it coerces types where the conversion is unambiguous. The parsing aspect is often understated. Pydantic is not only checking that a value is an int — it converts the string "5" from a query parameter into the integer 5. That is why you can declare a handler parameter as int and receive an int, despite HTTP carrying only strings. Beyond types, it enforces constraints: minimum and maximum values, string lengths, patterns, and custom validators. On failure it produces a structured error listing every field that failed and why, which FastAPI turns into a 422 response. That per-field detail is what makes the API usable for integrators, and getting it for free is a large part of the framework's value. It also serialises: converting the model back to JSON, handling datetimes, UUIDs, enums and nested models. And the model is the source of the OpenAPI schema. So one declaration gives parsing, validation, serialisation, documentation and static type checking — which is the whole design idea.

16

What changed between Pydantic v1 and v2?

The validation core was rewritten in Rust, which made it substantially faster — commonly cited as five to fifty times depending on the workload. For an API where every request is validated, that is a meaningful share of request time. The API changed considerably, which is the practical concern. Method names moved: dict() became model_dump(), json() became model_dump_json(), parse_obj became model_validate. Config as an inner class became model_config as a dict. validator became field_validator, and root_validator became model_validator. Semantics tightened too. Coercion is stricter — v1 would accept some conversions that v2 rejects — and smart union resolution changed how ambiguous types are matched. Optional no longer implies a default of None, so a field typed Optional[str] is required unless you give it a default. That single change breaks a lot of v1 code quietly, because the field becomes mandatory. The migration path: bump-pydantic automates much of it, and v2 ships a pydantic.v1 compatibility module. FastAPI supports both, but v2 is the current baseline.

17

Should you use the same Pydantic model for input and output?

No, in almost all cases. Separate models for the request and the response. The reasons are concrete. Input and output have different fields: a create request has a password, the response must not. The response has server-generated fields — id, created_at — that a request cannot supply. Making them one model means either optional fields everywhere, which weakens validation, or exposing fields you did not intend. The security case is the strongest: a shared model used as a response serialises whatever the object has, which is how password hashes and internal flags leak. The usual shape is a base model with the shared fields, a Create model adding write-only fields, an Update model with everything optional for PATCH, and a Read or Out model adding server-generated fields. The cost is more classes and some duplication, which people object to. The answer is that the duplication is meaningful — these are genuinely different contracts that evolve independently, and collapsing them couples your API surface to your internal representation. The same argument applies to using ORM models as schemas, which is worse still.

18

How do you write a custom validator?

field_validator for a single field, decorating a classmethod that receives the value, validates or transforms it, and returns it. Raising ValueError produces a proper 422 with the message. model_validator for validation involving several fields — checking that an end date is after a start date, or that exactly one of two optional fields is provided. It runs after field validation by default, with mode="before" available to transform the raw input first. The design guidance: validators should validate and normalise, not perform I/O. A validator that queries the database to check uniqueness couples your schema to your data layer, is hard to test, and runs before you have transaction context. That check belongs in the service layer. Normalisation is a legitimate use — stripping whitespace, lowercasing an email, parsing a flexible date format — and doing it in the model means it happens once at the boundary rather than being repeated. Annotated types with constraints often replace a validator entirely: a field typed with a length or range constraint needs no code. And keep error messages useful, since they reach the client.

19

What is the difference between Optional, a default, and exclude_unset?

Optional[str] means the type may be None. In Pydantic v2 it does not imply a default, so the field is still required — you must pass None explicitly. That changed from v1 and catches people migrating. A default value makes the field optional in the request: omitting it uses the default. The distinction that matters for PATCH is between a field omitted and a field explicitly set to null. Omitted usually means "leave it alone"; null usually means "clear it". If both produce None on the model, you cannot tell them apart, and you cannot support clearing a value. model_dump(exclude_unset=True) solves this: it returns only fields that were actually provided in the input, so you can apply exactly those. That is the correct implementation of PATCH semantics and it is frequently missed — the naive implementation overwrites every field with its default. exclude_none omits fields that are None regardless of whether they were set, which is a different thing and not what you want for PATCH. For an explicit three-state field, some codebases use a sentinel type to distinguish absent from null.

20

How do you validate nested and list data?

Pydantic handles it structurally: a field typed as another model validates recursively, and a field typed list[Item] validates every element. Errors carry the full path — the location in a validation error is a tuple like ("body", "items", 2, "quantity") — so the client learns exactly which element of which list failed, which is what makes the error usable. The practical considerations. Bound list lengths, because an unbounded list is a denial-of-service surface — a request with a million items consumes memory and CPU before your handler runs. Field with max_length on the list is the fix. Deeply nested models are expensive to validate, so very large payloads have a real cost. Recursive models — a comment with replies — work, but need a forward reference and can produce unbounded depth, so limiting nesting matters for the same reason. For a body that is a bare list rather than an object, declare the parameter as list[Item] directly. And for heterogeneous unions, use a discriminated union with a literal field, which is both faster and produces much clearer errors than trying each variant.

21

What is model_config and what settings matter?

model_config replaces v1's inner Config class and holds per-model settings. from_attributes, formerly orm_mode, lets a model be constructed from an arbitrary object by reading attributes rather than keys. That is what allows returning a SQLAlchemy row and having it serialised through a response model. extra controls unknown fields: ignore is the default, forbid rejects them with an error, and allow keeps them. forbid is worth considering for request models, because it catches client typos immediately rather than silently discarding the field — a client sending "quantitiy" otherwise gets a default and no warning. populate_by_name allows using the field name when an alias is defined, which matters for camelCase APIs. alias_generator applies a naming convention across all fields, which is how you serve camelCase JSON from snake_case Python without annotating every field. str_strip_whitespace normalises input. frozen makes the model immutable and hashable. The extra setting is the one most worth thinking about deliberately, since the default of silently ignoring unknown fields hides integration mistakes.

22

How do you handle camelCase JSON with snake_case Python?

Field aliases. Declare the Python field in snake_case and give it an alias matching the JSON key, so parsing accepts the alias and serialisation emits it. Doing that per field is tedious, so alias_generator applies a function across the whole model — to_camel is provided — which handles it in one line. The details that matter. populate_by_name allows the model to also accept the Python field name, which is useful for constructing instances in code and in tests without using the alias. For serialisation, model_dump(by_alias=True) emits the aliases; without it you get the Python names, which is a common source of "the response is snake_case despite the aliases". In FastAPI, response_model_by_alias defaults to true, so responses use aliases automatically. Pydantic v2 also separates validation_alias and serialization_alias, so you can accept one name and emit another — useful when migrating a field name without breaking clients. The alternative is to just use snake_case in the API, which many APIs do. Consistency matters more than the convention, and translating at the boundary is only worth it if consumers expect camelCase.

23

What is a discriminated union and why use one?

A tagged union where one field — the discriminator — identifies which variant a payload is. Declared with Field(discriminator="type") over a union of models, each with a Literal type field. Without it, Pydantic tries each variant in turn until one validates. That has two problems: it is slower, since several validation passes may run; and the error message on failure lists every variant's failures, which is nearly unreadable for a client. With a discriminator, Pydantic reads the tag, selects the one model, and validates against it. One pass, and the error is specific to that variant. The use cases: a webhook endpoint receiving several event types, a polymorphic payment method, a message envelope with a type field. It also produces a much better OpenAPI schema, using oneOf with a discriminator mapping that client generators understand — so generated clients get proper sum types rather than a loose union. The requirement is that every variant has the same discriminator field name with a distinct Literal value. It is one of the higher-value Pydantic features for real API design and is not widely known.

24

How should you represent money and decimals?

Not as a float. Binary floating point cannot represent most decimal fractions exactly, so arithmetic accumulates error and comparisons behave surprisingly. The options in Pydantic: Decimal, which is exact and validates correctly, with max_digits and decimal_places constraints available. Or an integer in the currency's minor unit — paise, cents — which is what Stripe does and is unambiguous. The serialisation question is where care is needed. JSON has no decimal type, so a Decimal serialised as a JSON number becomes a float on the other side and precision is lost in the consumer even though it was exact in your process. Serialising as a string preserves it, which is why many financial APIs return amounts as strings. Either way, always pair the amount with a currency code. A bare number is meaningless, and not every currency has two decimal places — yen has none, dinar has three — so assuming two is a bug waiting for internationalisation. And be consistent about whether integer amounts are minor units, documenting it explicitly, since that is the single most common integration mistake with payment APIs.

25

What is Pydantic Settings and why use it for configuration?

pydantic-settings provides a BaseSettings class that reads values from environment variables and .env files, validating them against the declared types. The value is that configuration becomes typed and validated at startup. A port declared as int is parsed and rejected if it is not a number. A required setting that is missing fails the process immediately with a clear message, rather than producing a confusing error deep in a request later. That startup validation is the main benefit — configuration errors are found at deploy time, not at 3am. It also solves the boolean problem: os.environ.get("DEBUG") returns the string "False", which is truthy, and a naive check enables debug in production. A typed bool field parses it correctly. Other benefits: defaults in one place, nested settings via delimiters, secrets read from files for container secret mounts, and a single object to pass around rather than scattered os.environ calls. The practical cautions: use SecretStr for sensitive values so they are redacted in repr and logs, and cache the settings instance with lru_cache so it is constructed once and can be overridden in tests.

26

How do you handle validation of query parameters and filters?

Declare them with Annotated and Query, adding constraints — ge, le, min_length, max_length, pattern — which are enforced and documented automatically. For a list-valued parameter, declare list[str] and Query, which accepts repeated occurrences of the key. For a set of related filters, a Pydantic model as a dependency is cleaner than a long parameter list: declare the model, use Depends, and the fields become query parameters. That groups them, allows model-level validation across fields, and keeps the handler signature short. The things that matter beyond syntax. Bound pagination parameters — a limit with a maximum, or a client will request a million rows. Allowlist sortable fields rather than passing the client's string into a query, which is both an injection risk and a way to trigger an unindexed sort. Provide sensible defaults so the endpoint is usable without every parameter. And be deliberate about unknown query parameters: FastAPI ignores them silently, so a client with a typo in a filter name gets unfiltered results rather than an error — which is a genuinely dangerous default for a delete-by-filter endpoint.

27

What is the performance cost of Pydantic validation?

Real but usually not dominant. Pydantic v2's Rust core made it fast enough that for a typical API request — a small body, a database query, a small response — validation is a small fraction of the total, with the database dominating. Where it becomes significant: very large response payloads, since every field of every object is validated and serialised on the way out; deeply nested models; and lists with many elements. The mitigations. For large responses, consider whether you need the response model at all — returning a Response with pre-serialised content skips validation, at the cost of losing the filtering guarantee and the schema. That trade is only worth making with measurement. Use a projection so you are not serialising fields nobody reads. Paginate, so no response is unboundedly large. And for the highest-throughput endpoints, ORJSONResponse replaces the default JSON encoder with orjson, which is considerably faster and handles datetimes natively. The general guidance is to profile before optimising this, because the instinct to blame validation is usually wrong — it is normally the query.

28

How do you convert between ORM models and Pydantic schemas?

Set from_attributes in model_config, which lets Pydantic read attributes rather than dict keys, then validate the ORM object against the schema — model_validate(obj), or automatically via response_model. The reason to keep them separate rather than using the ORM model directly: the schema defines the API contract while the ORM model defines storage, and they change for different reasons. Coupling them means a column rename is a breaking API change and every new column is silently published. The practical hazards. Lazy loading: serialising an ORM object with an unloaded relationship triggers a query per object, so a list response produces N+1 — and in async code it may raise instead, because lazy loading needs a synchronous session. Eager-load what the schema needs. The conversion also forces full serialisation of everything in the schema, so a schema including a large relationship is expensive. For read-heavy endpoints, querying directly into the shape you need — selecting specific columns — avoids loading the ORM object at all and is both faster and simpler. And never let an ORM model be the response model.

29

What happens when validation fails, and how do you customise it?

FastAPI returns 422 Unprocessable Entity with a body listing each error: its location as a path, a message, and a type code. That structure is genuinely useful — a client can map errors back to specific form fields — and it is one of the better default behaviours in the framework. To customise, add an exception handler for RequestValidationError. That lets you reshape the response into whatever error envelope your API uses, so validation errors match the format of your other errors rather than being the odd one out. The reasons to customise: consistency with the rest of your error contract; changing the status code to 400 if that is your convention, since 422 is sometimes rejected by intermediaries or unfamiliar to clients; and controlling what is echoed back. That last point matters for security. The default includes the input value in the error, so a validation failure on a password field can echo the submitted password into logs and responses. Stripping the input from the error output is worth doing deliberately. And log validation failures at a level that lets you spot a client integrating incorrectly.

30

How do you version an API in FastAPI?

The most common approach is URL path versioning with a separate router per version, included under /v1 and /v2 prefixes. That is explicit, visible in logs, and easy to route at a proxy. The implementation question is how to avoid duplicating everything. The usual pattern is shared service and domain logic with version-specific routers and schemas, so v1 and v2 differ only in their API surface and both call the same underlying functions. Duplicating the business logic per version is where this becomes unmaintainable. Separate schema modules per version are worth having, because the whole point is that the contract can differ. Header-based versioning is possible with a dependency reading the header, but it is invisible in logs and harder to test manually. The more important guidance is to avoid needing versions. Additive changes — new optional fields, new endpoints — do not require one. A version is needed only for breaking changes, and each one is code you maintain and test indefinitely. So the discipline is designing for additive evolution and treating a new version as a last resort.

31

How does FastAPI's dependency injection work?

A dependency is a callable declared with Depends. FastAPI inspects its signature, resolves its own parameters — including nested dependencies — calls it, and passes the result to your handler. So dependencies are recursive: a dependency can depend on other dependencies, and FastAPI builds the whole graph per request. What makes it distinctive is that dependencies participate fully in the request model. They can declare path, query, header and body parameters, and those appear in the OpenAPI schema — so a dependency that reads an API key documents that header on every route using it. They can also raise HTTPException, which is what makes authentication and authorisation natural as dependencies: the check happens before the handler runs, and failing short-circuits with the right status. A dependency using yield gets teardown after the response, which is how database sessions are managed. The practical value is that cross-cutting concerns — auth, sessions, pagination parameters, rate limits — become declarative and testable, and they are visible in the handler signature rather than hidden in middleware.

32

What is a yield dependency and when does the teardown run?

A dependency written as a generator with yield provides the value before the yield and runs cleanup after. The cleanup runs after the response has been generated — after the handler returns and after the response model serialisation, but before the response is sent in older versions and after it in newer ones, which matters for background work. The canonical use is a database session: create it, yield it, and close it in a finally block so it closes even if the handler raises. The try/finally is essential. Without it, an exception in the handler skips the cleanup and the session leaks — which under load exhausts the connection pool. The subtleties worth knowing. Exceptions from the handler propagate into the dependency at the yield point, so you can catch them there to roll back a transaction. Since FastAPI 0.106, you cannot raise HTTPException in the teardown and have it affect the response, because the response is already determined. And a StreamingResponse's generator runs after teardown, so a session closed by the dependency is unusable inside it — a genuinely confusing failure.

33

How do you manage a database session per request?

A yield dependency that creates a session, yields it, and closes it in a finally block. Every handler needing the database declares it as a parameter. That gives one session per request with guaranteed cleanup, which is what you want — a session shared across requests is a correctness and concurrency problem. The transaction question is the design decision. The simplest approach is to commit explicitly in the service layer and roll back on exception in the dependency. The alternative is a unit-of-work pattern where the dependency opens a transaction and commits on success, so handlers never call commit — which is cleaner but makes partial commits impossible. For async, the session must be an AsyncSession and the dependency an async generator, or you block the event loop on every query. The pool matters as much as the session. The pool size bounds concurrent database work, and a pool much larger than the database can serve converts a queue you can measure into contention you cannot. Sizing it well below the connection limit, accounting for the number of worker processes, is the part people get wrong — each worker has its own pool, so the total is pool size times workers.

34

What is dependency caching and when does it bite?

Within a single request, FastAPI calls each dependency once and reuses the result if it appears several times in the graph. So a get_current_user dependency used by three nested dependencies runs once, not three times. That is almost always what you want, and it is why building layered dependencies is cheap. The caching key is the dependency callable and its parameters, and it is per request — nothing is shared between requests. Where it bites: when you deliberately want two independent instances. Depends(get_thing, use_cache=False) disables it for that occurrence. The more common confusion is expecting caching across requests. A dependency doing expensive setup — loading a model, reading configuration — runs on every request unless you cache it yourself, typically with lru_cache on the underlying function. That combination, an lru_cache function wrapped in a dependency, is the standard way to get a per-process singleton. And because caching is per request, a dependency with side effects runs once per request regardless of how many places request it, which is usually right but worth knowing when the side effect is what you wanted.

35

What is a class-based dependency and when is it useful?

A class whose __init__ declares the request parameters can be used directly as a dependency — FastAPI treats __init__ as the callable, so the parameters become query or path parameters and the instance is passed to the handler. The common use is grouping related parameters: a Pagination class taking skip and limit with defaults and constraints, declared once and reused across every list endpoint. That is cleaner than repeating two parameters everywhere and gives one place to change the maximum. The other pattern is a callable class — one implementing __call__ — which lets you parameterise the dependency at construction. A RoleChecker constructed with a required role, used as Depends(RoleChecker("admin")), is the standard way to build a permission dependency without a factory function. That parameterisation is the thing a plain function dependency cannot do directly, since Depends takes the callable rather than a call. The alternative is a dependency factory — a function returning a function — which achieves the same thing and reads a little less clearly. Either way, keeping the class small and free of I/O in __init__ keeps it testable.

36

How do you apply a dependency to every route in a group?

Pass it in the dependencies list on the APIRouter constructor, or on include_router, or on the FastAPI application for global application. Those dependencies run for every route in the scope but their return value is not passed to the handler — which is exactly right for checks that either pass or raise, such as authentication or a rate limit. The practical use is securing a whole section: an admin router constructed with a dependency requiring an admin role means every route in it is protected, and adding a new route cannot accidentally omit the check. That fail-safe property is the main argument for it over per-route dependencies. A per-route decorator is easy to forget on the one endpoint that matters. The caveat is that a global dependency applies to everything including health checks and the docs, so you generally want it at the router level rather than the application level, or you need exclusions. And because the value is discarded, a dependency that both authorises and returns the user needs to be declared at the route level too if the handler needs the user — the router-level one runs, and the route-level one is served from the per-request cache, so it costs nothing.

37

How do you override dependencies in tests?

app.dependency_overrides is a dict mapping the original dependency callable to a replacement. Anything in it is substituted when the graph is resolved. That is the mechanism that makes FastAPI applications genuinely testable: replace the database session dependency with one bound to a test database or a transaction that rolls back; replace the current-user dependency with one returning a fixed user, so tests do not need to construct real tokens; replace an external client with a fake. The practical points. Clear the overrides between tests, or state leaks — a fixture that sets and then clears is the standard pattern, and forgetting produces order-dependent failures that are hard to trace. Override at the right level: overriding the session dependency is usually better than overriding the repository, because it tests more of the real stack. The key is the exact callable object, so if the dependency is wrapped or imported differently the override silently does not apply — the same class of confusion as patching the wrong name. This is a genuine advantage over frameworks where dependencies are resolved via imports, which require monkeypatching instead.

38

Should business logic live in dependencies or in services?

In services. Dependencies should acquire and validate the things a handler needs — a session, the current user, parsed pagination — not implement the operation. The reasons. A dependency is coupled to the request context, so logic living there cannot be reused from a background job, a CLI command or another service. It also cannot be tested without constructing a request. Dependencies also run before the handler, so ordering business logic among them is implicit and hard to follow. The structure that works: the handler receives the session and the user from dependencies, calls a service function passing them, and shapes the response. The service contains the logic and knows nothing about HTTP — no HTTPException, no Request object. That separation means the service is unit-testable without the framework, and the same operation can be triggered from a queue consumer. The pragmatic exception is authorisation, which is legitimately a dependency because it must run before the handler and short-circuits with a status code. But even there, the decision of whether a user may act on a specific resource often belongs in the service, since it depends on data.

39

How do you share expensive resources across requests?

Create them once at startup in the lifespan context manager and store them on app.state, then expose them through a dependency that reads from the request's app. That is right for connection pools, HTTP clients, message broker connections and loaded models — things that are expensive to create and safe to share. The alternative for simple cases is a module-level function wrapped in lru_cache, used as a dependency. It is created lazily on first use and reused thereafter, which is simpler but gives no shutdown hook. The scope to be clear about is per process, not global. With several Uvicorn workers, each has its own copy — so a pool of ten with four workers means forty connections to the database, which is the sizing mistake people make. What must not be shared: anything holding per-request state, and anything not safe for concurrent use. A database session is per request; a connection pool is shared. And everything created at startup must be closed at shutdown, or a rolling deployment leaves connections dangling until they time out on the server.

40

What is the Annotated style for dependencies and why is it preferred?

Instead of a default value — user: User = Depends(get_current_user) — you write user: Annotated[User, Depends(get_current_user)]. The reasons it is now recommended. It separates the dependency declaration from the default value, so a parameter can have both a dependency and a real default, which the old style could not express. It makes the annotation reusable: you can define CurrentUser = Annotated[User, Depends(get_current_user)] once and use that alias everywhere, which removes repetition and gives one place to change the dependency. It works correctly with static type checkers and editors, which previously saw the parameter as having a Depends object as its default rather than a User. And it allows calling the function directly outside a request — from a script or a test — because the parameter has no strange default value. The same applies to Query, Path, Header and Body, which are all now recommended inside Annotated. The old style still works and a great deal of existing code and documentation uses it, so both are worth recognising.

41

How do you implement pagination as a reusable dependency?

A class or function declaring the pagination parameters with constraints, returning a small object the handler passes to the query layer. The constraints are the point: a limit with a maximum, and an offset or cursor with a minimum. Bounding the limit in one place means no endpoint can be asked for a million rows. The design decision is offset versus cursor. Offset is simple and allows jumping to a page, but deep offsets are slow because the database scans and discards, and concurrent inserts shift items between pages. Cursor pagination is stable and fast at any depth but only supports sequential navigation. For an internal admin list, offset is fine. For a public or large dataset, cursor is the right default. The response shape should be consistent across endpoints — items plus next cursor, or items plus total — and defining it as a generic Pydantic model gives one contract and correct OpenAPI schemas for every paginated endpoint. The total count is worth making optional, since the count query can cost more than the page itself on a large filtered table.

42

What happens if a dependency raises an exception?

It short-circuits: the handler never runs, and the exception propagates through the normal handling. If it is an HTTPException, FastAPI returns the corresponding status and detail. That is the intended mechanism for authentication and authorisation — the dependency raises 401 or 403 and the handler is never entered, so it can assume a valid authenticated user. If it is another exception, it reaches your exception handlers or produces a 500. The ordering consequence worth knowing: dependencies are resolved in the order needed to build the graph, so an authentication dependency raising 401 prevents a later, more expensive dependency from running at all. Putting cheap checks before expensive ones therefore matters — an unauthenticated request should not open a database session. For teardown, a yield dependency that has already yielded still runs its cleanup, so a session opened before a later dependency failed is still closed properly. The practical implication for design is that raising from a dependency is the idiomatic way to reject a request, rather than returning a sentinel the handler must check — which is easy to forget and produces a silent security hole.

43

How do you inject the current user and enforce permissions?

A chain of dependencies. The first extracts and validates the token — from an Authorization header via a security scheme — and returns the user, raising 401 if it is missing or invalid. A second builds on it to check status, raising 403 for a disabled account. For role or permission checks, a parameterised dependency is the clean pattern: a callable class constructed with the required permission, used as Depends(RequirePermission("orders:write")). That reads well at the route and keeps the logic in one place. Because of per-request caching, the token is decoded once no matter how many of these are in the graph. The design point worth making is the distinction between authorisation that depends only on the user and authorisation that depends on the resource. "Is this an admin?" is a dependency. "Does this user own order 123?" usually is not — it needs the order loaded, so it belongs in the service, where the ownership check can be part of the query rather than a separate fetch-then-check that races. Scoping the query by the user is the more robust pattern.

44

What is the difference between a dependency and middleware for cross-cutting concerns?

Middleware wraps the whole request-response cycle, runs for every request including unmatched paths, and has no access to the resolved dependency graph or to which route will handle the request. A dependency runs inside the routing system, so it knows the route, participates in the OpenAPI schema, can declare its own parameters, and can short-circuit with a proper status code. The rule that follows: use middleware for things that are genuinely global and route-independent — request IDs, timing, CORS, compression, catching unhandled exceptions. Use dependencies for anything that varies by route or needs request context: authentication, authorisation, database sessions, feature flags, per-endpoint rate limits. Authentication in middleware is a common choice and usually the wrong one. It cannot easily exempt public routes without path matching, it does not appear in the API documentation, and it cannot pass the user to the handler cleanly — you end up stashing it on request.state, which is untyped and invisible in the signature. The dependency version documents itself, is testable in isolation, and is overridable in tests.

45

What is the difference between def and async def route handlers?

An async def handler runs directly on the event loop. A plain def handler is run in a thread pool, because running it on the loop would block everything. That automatic threadpool offload is a deliberate design decision and it is why FastAPI works well with synchronous libraries — a handler using a blocking database driver declared as def is safe. The consequences. A def handler consumes a thread from a pool with a default size around forty, so concurrency for synchronous handlers is bounded by that, not by the event loop. An async def handler must not block, because there is nothing to protect the loop. A synchronous database call inside async def stalls every concurrent request on that worker. So the rule is: if your libraries are async, use async def. If they are synchronous, use plain def and let FastAPI handle it. The one thing you must not do is use async def and then call blocking code inside it. Mixing is fine — some routes async, some sync — and choosing per route based on what the handler actually calls is the correct approach.

46

What happens if you block the event loop?

Every concurrent request on that worker process stops making progress until the blocking call returns. The loop is single-threaded and cooperative, so a coroutine that does not await cannot be preempted. A one-second blocking call means every other in-flight request waits an extra second. The symptoms are distinctive and confusing: throughput far below expectation, latency that grows with concurrency rather than staying flat, health checks timing out under load, and CPU that does not look saturated. Nothing in the application logs points at the cause, because the blocking call succeeds normally. The common culprits: a synchronous HTTP client such as requests inside an async handler; a synchronous database driver; file I/O; a CPU-heavy computation; and time.sleep instead of asyncio.sleep. The fixes: use async libraries — httpx or aiohttp, asyncpg or an async SQLAlchemy driver. Where none exists, offload with asyncio.to_thread or run_in_executor. For CPU work, use a process pool. The diagnostic is asyncio debug mode, which logs any callback taking longer than a threshold — that finds the blocking call quickly, and it is worth enabling in a staging environment.

47

When does async actually improve performance?

When the work is I/O-bound and there is meaningful concurrency. A handler that waits on a database, an HTTP call, or a cache spends most of its time idle. Async lets that idle time be used by other requests, so one process handles far more concurrent requests than it has threads. It helps most with many concurrent slow operations — a handler fanning out to several services, long-polling, WebSockets, or streaming. Where it does not help: CPU-bound work, which occupies the loop regardless. And a low-concurrency service, where there is nothing to overlap — a handler that takes 50ms of database time is 50ms either way. The honest observation is that for a typical CRUD service, the difference between an async stack and a synchronous one with a thread pool is often small, and dominated by the database. The gains people expect from switching to async frequently do not materialise, because the bottleneck was never concurrency. So the question to ask is what the actual constraint is. If the database is saturated, async makes no difference; if the service is holding thousands of slow outbound calls, it makes a large one.

48

How do you call several services concurrently in a handler?

asyncio.gather, or a TaskGroup on Python 3.11 and later, so the calls overlap rather than running one after another. Awaiting them sequentially is the common mistake — three 100ms calls take 300ms sequentially and about 100ms concurrently. Code that is syntactically async but semantically sequential gains nothing, and it looks correct. The error handling decision matters. gather by default propagates the first exception while leaving the others running unawaited, which produces warnings and orphaned work. return_exceptions=True gives you results and exceptions together so you can decide per call — which is right when a partial result is acceptable. TaskGroup is the better modern option because it cancels the remaining tasks when one fails and guarantees nothing outlives the block. Other considerations: set a timeout on the whole group, so one slow dependency cannot hold the request indefinitely. Bound the concurrency if the fan-out is large, since a hundred simultaneous outbound calls may overwhelm the target. And decide explicitly whether a failed optional call degrades the response or fails the request.

49

How does the thread pool for sync handlers work and can you exhaust it?

Plain def handlers and sync dependencies are run in an anyio thread pool, which defaults to forty threads. Yes, it can be exhausted. Forty concurrent slow synchronous handlers occupy every thread, and further requests queue waiting for one — so latency rises sharply and the service appears hung while CPU is idle. That is a real production failure mode and it is not obvious, because nothing errors; requests simply wait. The limit is per process, so the total across workers is forty times the worker count. The responses: raise the limit if the work is genuinely I/O-bound and the downstream can take it, by configuring the anyio capacity limiter at startup. But raising it is bounded by memory, since each thread has a stack. Better, move to async handlers with async libraries so concurrency is not thread-bound. Or reduce the time each handler holds a thread — which usually means fixing a slow query or adding a timeout to an outbound call. The diagnostic is comparing request concurrency against the limit, and noticing that latency rises while CPU does not.

50

What is the right async database setup?

An async driver and an async session throughout: asyncpg for PostgreSQL, and SQLAlchemy's async engine with AsyncSession, or an alternative like Tortoise or Piccolo. The session dependency becomes an async generator yielding an AsyncSession, and queries are awaited. The traps specific to async SQLAlchemy. Lazy loading does not work — accessing an unloaded relationship raises MissingGreenlet rather than issuing a query, because lazy loading needs synchronous I/O. So relationships must be eagerly loaded with selectinload or joinedload in the query, which is arguably a benefit since it forces you to be explicit and prevents accidental N+1. The pool must be sized with worker count in mind: pool size times workers is the real connection count against the database, and exceeding its limit is a common outage. And if you use a connection pooler such as PgBouncer in transaction mode, prepared statement caching must be disabled, or you get errors that appear intermittently under load. Half-migrating — an async session with a sync driver, or one blocking call in a chain — gives you the complexity without the benefit.

51

How do you make outbound HTTP calls correctly?

Use httpx with an AsyncClient in async handlers, and reuse a single client for the lifetime of the application rather than creating one per request. Creating a client per request means a new connection pool each time, so every call pays a TCP and TLS handshake, and connections are never reused. That is a large and easily-avoided latency cost, and it eventually exhausts ephemeral ports under load. So create the client in the lifespan, store it on app.state, and expose it through a dependency. Close it on shutdown. Always set timeouts — httpx defaults to five seconds, but the important thing is setting them deliberately per call type, including connect and read separately. A call with no timeout can hold a request indefinitely. Configure connection limits so one slow downstream cannot consume unbounded connections. Add retries with backoff for idempotent requests only, and a circuit breaker for a dependency that fails persistently, so you fail fast instead of holding requests. And propagate the trace context header, or distributed tracing breaks at every outbound call. Using requests inside an async handler is the mistake to avoid entirely.

52

What is the difference between BackgroundTasks and a task queue?

BackgroundTasks runs in the same process after the response. A task queue — Celery, ARQ, Dramatiq — hands the work to a broker and a separate worker process executes it. The differences that matter: persistence, retries and isolation. A background task is lost if the process restarts, crashes, or is terminated during a deployment. There is no retry, no visibility, and no record that it was supposed to happen. A queued task survives, is retried on failure, and can be inspected. A background task also competes with request handling for the same process resources, so a slow one reduces capacity. So the decision is about whether the work must happen. Sending a non-critical notification, warming a cache, writing an analytics event — a background task is proportionate. Charging a card, sending an order confirmation, processing an upload — those need a queue. The intermediate option is writing the intent to your database in the same transaction as the state change, and having a worker poll it. That gives durability without a broker, and it is the outbox pattern. Using BackgroundTasks for work that matters is a quiet, common mistake.

53

How do you handle timeouts for a request?

At several layers, and they must be consistent. Within the handler, wrap outbound calls with asyncio.timeout so a slow dependency cannot hold the request indefinitely. Each client should also have its own timeout as a backstop. An overall request timeout can be applied with middleware wrapping the handler in a timeout, returning 504 on expiry — FastAPI has no built-in request timeout, which surprises people. At the server, Uvicorn has keep-alive and graceful shutdown timeouts but not a per-request one, so the reverse proxy is where a hard limit usually lives. The structural rule is that timeouts must decrease as you go deeper. If the proxy times out at 30 seconds and your handler waits 60 on a database call, you return 504 while the handler continues working on a request nobody is waiting for — consuming a connection and a worker slot. Propagating a deadline downward is the correct fix. And cancellation must be handled: when the client disconnects, the task is cancelled, and any cleanup must run in a finally block or resources leak.

54

What happens when a client disconnects mid-request?

The ASGI server cancels the task running the handler, which raises CancelledError at the next await point. That is usually desirable — work for a client that has gone away is wasted — but it has consequences. Cleanup must be in finally blocks or context managers, or a cancelled request leaks its database session, its transaction, or its lock. That is a real source of pool exhaustion under load with impatient clients. Partial work is a correctness question. A handler cancelled halfway through a multi-step operation leaves it partly done unless the whole thing is in a transaction. So anything with side effects should either be transactional or be idempotent on retry. Cancellation is cooperative, so a handler in a blocking call or a CPU loop cannot be cancelled and runs to completion regardless. await request.is_disconnected() lets a long-running handler check and stop early, which is worth doing in a streaming or long-polling endpoint. And CancelledError must not be swallowed — catching it to clean up and re-raising is correct, catching and continuing breaks the semantics and leaves work running after the caller has gone.

55

How do you run CPU-bound work in a FastAPI service?

Not on the event loop, and preferably not in the service at all. Inside an async handler, offload to a process pool with run_in_executor, since a thread pool does not help — the GIL means CPU-bound Python in a thread still contends. The cost is pickling the arguments and results. A plain def handler already runs in a thread, which avoids blocking the loop but still competes for the GIL, so it does not give parallelism. The better architecture for anything substantial is to move it out of the request path entirely: enqueue the work, return 202 with a status URL, and process it in a worker. That keeps the API responsive and lets the compute scale independently. For moderate work — image resizing, a PDF, a report — a process pool created at startup and shared is reasonable. The measurement to make first is how long it actually takes. A 5ms computation does not need any of this; a 5-second one certainly does. The threshold where it starts hurting is lower than people expect, because it blocks every concurrent request for its full duration.

56

What are the most common async mistakes in FastAPI code?

Declaring a handler async def and then calling a synchronous library inside it, which blocks the loop for every concurrent request. This is by far the most common and the most damaging. Awaiting several calls sequentially when they could run concurrently. Forgetting to await a coroutine, which creates it and never runs it — producing a warning that is easy to miss and a handler that silently does nothing. Using time.sleep instead of asyncio.sleep. Creating an HTTP client per request rather than reusing one. Using async def for a handler that does no I/O at all, which gains nothing and risks the blocking mistake later. Lazy-loading ORM relationships in async SQLAlchemy, which raises rather than working. Not handling cancellation, so cleanup is skipped when clients disconnect. Unbounded gather over a large list, exhausting connections or memory. And mixing sync and async database sessions in the same request. The root cause of most of them is treating async as a keyword rather than an execution model, and the fastest way to catch them is asyncio debug mode plus a linter that flags un-awaited coroutines.

57

How do you decide between sync and async for a new service?

Start from what the service does and what libraries exist for it. If the service is mostly database reads and writes with a mature synchronous driver, a sync stack with plain def handlers is simpler, and FastAPI's thread pool handles concurrency adequately. Debugging is easier, stack traces are shorter, and there is no risk of accidentally blocking a loop. If the service makes many concurrent outbound calls, holds long-lived connections, or needs very high connection concurrency, async is the right model and the gains are real. The deciding constraint is often library maturity. If a key dependency has no async version — a particular database driver, an SDK — then going async means wrapping it in threads anyway, which gives you the complexity without much benefit. The advice worth giving is not to choose async by default for performance reasons without measuring. For most CRUD services the database is the bottleneck and the concurrency model is not. And mixing is legitimate: async handlers where they help, sync handlers where the libraries are sync, in the same application.

58

How does Uvicorn with multiple workers interact with async?

Each worker is a separate process with its own event loop, its own memory and its own connection pools. Async gives concurrency within a process; multiple workers give parallelism across cores. You need both — a single async worker uses one core regardless of how many concurrent requests it juggles. The usual rule of thumb is workers roughly equal to cores, adjusted by measurement. The consequences of process separation are the things people get wrong. In-memory state is per worker: a cache, a rate limiter, or a set of WebSocket connections exists independently in each, so behaviour depends on which worker served the request. Anything that must be shared needs Redis or the database. Connection pools multiply: a pool of ten with four workers is forty connections, which is how database connection limits get exhausted. Startup code in the lifespan runs once per worker, so expensive initialisation is paid per process — and a model loaded per worker multiplies memory. In production the common setup is Gunicorn managing Uvicorn workers, or a container per worker with the orchestrator handling scaling.

59

How do you implement JWT authentication in FastAPI?

A security scheme — OAuth2PasswordBearer or HTTPBearer — declares where the token comes from, which both extracts it and documents the requirement in OpenAPI so the Swagger UI gets an authorise button. A dependency then decodes and verifies the token, loads or reconstructs the user, and raises 401 if anything fails. The verification steps that must all be present: check the signature with the correct algorithm, explicitly specified rather than read from the token header; check expiry; check the issuer and audience if you set them. Omitting the algorithm allowlist is the classic vulnerability, since an attacker can present alg=none or switch to a symmetric algorithm using the public key as the secret. Use a maintained library — PyJWT or python-jose — rather than hand-rolling, and pass algorithms explicitly. The design decisions: short-lived access tokens with refresh tokens, because a JWT cannot be revoked before expiry. Store secrets in configuration, not code. And decide whether the token carries claims you trust — putting a role in the token means a permission change does not take effect until it expires. Hash passwords with bcrypt or Argon2, never a fast hash.

60

What is the difference between OAuth2PasswordBearer and HTTPBearer?

Both extract a bearer token from the Authorization header. The difference is what they declare in the OpenAPI schema and therefore how the documentation behaves. OAuth2PasswordBearer declares an OAuth2 password flow with a token URL, so Swagger UI shows a username and password form and calls your token endpoint to obtain a token. That is convenient for testing a service that issues its own tokens. HTTPBearer declares a plain bearer scheme, so the UI just asks for a token to paste. That is the honest choice when tokens come from an external identity provider and your service only validates them. Functionally, both give you the token string; the choice is about documentation accuracy. The detail worth knowing is auto_error. By default both raise 403 when the header is missing, which is arguably the wrong status — 401 with a WWW-Authenticate header is correct for missing credentials. Setting auto_error=False returns None instead and lets your dependency decide, which is what you want for endpoints that are optionally authenticated. Using the wrong status matters because clients branch on it to decide whether to refresh a token.

61

Should you store JWTs in localStorage or a cookie?

The trade is XSS exposure against CSRF exposure. A token in localStorage is readable by any JavaScript on the page, so a single XSS vulnerability exfiltrates it. It is not sent automatically, so CSRF is not a concern. A token in an HttpOnly cookie cannot be read by script at all, which removes the XSS exfiltration path. But the browser attaches it automatically to matching requests, which is exactly what makes CSRF possible. The modern consensus leans toward HttpOnly, Secure, SameSite cookies, because XSS is the more damaging and more common vulnerability, and SameSite=Lax — now the browser default — handles most of the CSRF surface. Adding an anti-CSRF token for state-changing requests closes the rest. The practical complications with cookies: cross-origin setups need SameSite=None with Secure, and the CORS configuration must allow credentials, which means the allowed origin cannot be a wildcard. For a same-origin web application, cookies are the better default. For a mobile client or a third-party API consumer, the Authorization header is the natural fit and localStorage is not involved. Either way, keep access tokens short-lived.

62

How do you handle token refresh?

Issue a short-lived access token and a longer-lived refresh token. The client uses the access token until it expires, then calls a refresh endpoint to obtain a new pair. The reason for the split is revocation. A JWT is valid until it expires and cannot be withdrawn, so a short lifetime bounds the damage from a leaked token. The refresh token is longer-lived but is presented rarely and can be stored server-side, so it can be revoked. The design points that matter. Store refresh tokens — hashed — so they can be checked against a revocation list, which is what makes logout meaningful. Rotate on use: each refresh issues a new refresh token and invalidates the old one. Combined with reuse detection — if an already-used refresh token is presented, revoke the whole family — that detects token theft, because the legitimate client and the attacker cannot both use the same chain. Refresh tokens should be HttpOnly cookies scoped to the refresh path, so they are not sent with every request. And handle the concurrent-refresh race: several parallel requests hitting a 401 simultaneously must not all refresh, or rotation invalidates each other.

63

How do you configure CORS correctly?

CORSMiddleware with an explicit list of allowed origins, methods and headers. The important rule is not to use a wildcard for origins in production. Allowing any origin means any website can make authenticated requests from a user's browser and read the responses. The combination that is actually forbidden by the specification is a wildcard origin with credentials — browsers reject it — so if you need cookies or the Authorization header on cross-origin requests, you must enumerate origins. The things to understand about what CORS is. It is enforced by the browser, not the server, and it protects the user rather than your API. A CORS error does not mean the request was blocked; for a simple request the server may have processed it fully and the browser merely refused to expose the response. curl and server-to-server calls are unaffected, which is why CORS is not an access control mechanism. So authorisation still has to be enforced server-side regardless. The practical debugging point: preflight OPTIONS requests must be answered, and a missing allowed header or method causes a failure that looks like the request never arrived.

64

How do you prevent SQL injection and other injection attacks?

Use parameterised queries. With SQLAlchemy that means bound parameters or the query builder, never f-strings or string concatenation with user input. The ORM protects you for values but not for identifiers — a column name in an order_by cannot be a bound parameter, so dynamic sorting is exactly where concatenation is tempting. The fix is an allowlist mapping client-supplied sort keys to known columns, rejecting anything else. The same applies to raw SQL through text(): use bound parameters. Beyond SQL, the injection surfaces in a typical service: shell commands, where subprocess must be called with a list and shell=False; path traversal, where a client-supplied filename must be sanitised and resolved against a base directory; and template injection if user input reaches a template engine. Server-side request forgery deserves a mention — an endpoint fetching a URL the client supplies can be used to reach internal services and the cloud metadata endpoint, so destinations need an allowlist and private ranges must be blocked after DNS resolution. Pydantic helps by constraining types and formats at the boundary, which eliminates a lot of malformed input before it reaches your logic.

65

How do you implement rate limiting?

With a dependency or middleware backed by shared state — Redis in practice, because in-memory state is per worker process and per container, so a limit of 100 with four workers is really 400. That multiplication is the mistake people make first. The algorithm choice: a token bucket allows bursts while enforcing an average rate and is a good default. A fixed window is simplest but permits double the limit across a boundary. A sliding window is more accurate and slightly more expensive. The implementation must be atomic — a Redis Lua script, or an INCR with an expiry — or concurrent requests race on the check-then-increment. The design decisions: what to key on. An API key or user ID is precise; an IP address is shared behind NAT and punishes everyone behind it. Different limits per endpoint make sense when some are far more expensive. Return 429 with Retry-After, and ideally expose the remaining quota on every response so well-behaved clients can pace themselves rather than discovering the limit by hitting it. slowapi is the common library, though it needs care in multi-worker deployments.

66

What security headers should a FastAPI service send?

For an API returning JSON, fewer matter than for a site serving HTML, but several still do. Strict-Transport-Security so browsers use HTTPS and the initial plaintext request cannot be intercepted. X-Content-Type-Options: nosniff, which stops content type guessing — relevant because a JSON response containing user content could otherwise be interpreted as HTML. Cache-Control: no-store on responses containing sensitive data, so they are not written to disk or held by intermediaries. If the service serves any HTML — including the interactive docs — Content-Security-Policy and X-Frame-Options matter. The practical point is that these are usually set at the reverse proxy rather than in the application, and doing it in one place is better than per-service middleware. Beyond headers, the API-specific concerns matter more: not returning stack traces or internal detail in errors, not leaking whether a user exists through differential error messages, redacting secrets from logs, and disabling or protecting the docs endpoints in production since they publish your entire API surface including internal endpoints.

67

How do you handle authorisation that depends on the resource?

In the service or query layer, not in a dependency — because the decision needs the resource loaded, and a dependency runs before the handler. The robust pattern is to scope the query by the principal rather than fetching then checking. Selecting the order where the id matches and the owner is the current user means the database cannot return someone else's row, so a missing ownership check is structurally impossible rather than merely forgotten. The fetch-then-check pattern works but has two weaknesses: it is easy to omit on one endpoint, and it introduces a window where the check and the use are separate. The status code decision matters here. Returning 403 for a resource that exists but is not yours confirms its existence, which is information disclosure when identifiers are guessable or sensitive. Returning 404 hides it. GitHub returns 404 for private repositories deliberately. For anything beyond ownership — role hierarchies, shared access, delegated permissions — centralise the policy rather than scattering conditionals, so it can be audited and tested in one place. And default to deny: a new endpoint without an explicit policy should fail closed.

68

How should passwords be stored and verified?

Hashed with a deliberately slow, salted algorithm — bcrypt, scrypt or Argon2 — never a fast hash like SHA-256, and never encrypted. The reason for slowness is brute force. A GPU computes billions of SHA-256 hashes per second, so a fast hash offers little protection once a database leaks. bcrypt and Argon2 have a tunable work factor that makes each attempt expensive. Argon2 is the current recommendation and won the Password Hashing Competition; bcrypt remains perfectly acceptable and is widely deployed. passlib is the usual library, though its bcrypt backend has had compatibility issues with newer bcrypt versions worth being aware of. Salting is handled by these algorithms automatically — the salt is embedded in the hash string — so you do not manage it yourself. The verification details: compare using the library's verify function, which is constant-time. Never compare hashes with ==. And the login endpoint should not reveal whether the username exists — the same error and, ideally, similar timing for both cases, since a fast rejection for unknown users leaks which accounts are real. Rate limit login attempts, and consider re-hashing on login when the work factor increases.

69

How do you protect the OpenAPI docs in production?

Either disable them or put them behind authentication. Disabling is one line: pass docs_url=None, redoc_url=None and openapi_url=None to the FastAPI constructor, conditionally based on the environment. The reason it matters is that the schema publishes your entire API surface — every endpoint including internal and administrative ones, every parameter, and every model field. That is a reconnaissance gift, and it frequently reveals endpoints the team did not realise were exposed. Protecting rather than disabling is often better for an internal API. You can define the docs routes yourself with a dependency requiring authentication, serving the Swagger UI HTML manually — FastAPI provides helpers for this — so authorised users keep the documentation. The alternative is network-level: expose the docs paths only on an internal listener or behind a VPN, blocking them at the proxy for public traffic. The related point is that the schema should not leak more than intended even when it is public: include_in_schema=False hides an individual route, which is the right treatment for internal endpoints in a service whose docs are otherwise published.

70

What is mass assignment and how does Pydantic help or hurt?

Mass assignment is binding request data directly onto a model or entity, so a client can set fields the endpoint never intended to expose — sending is_admin or role and having it applied. Pydantic helps by construction, because the request model declares exactly which fields exist and unknown ones are ignored by default. A client sending is_admin to an endpoint whose model lacks it has no effect. Where it hurts is when the same model is used for input and for the entity, or when the model includes fields that should be server-controlled. A UserCreate model containing a role field lets the client choose their role, which is the vulnerability restated. So the protections are: separate request models per operation with only the fields a client may set; never reuse a response or entity model as a request model; and be deliberate about which fields appear in an update model. Setting extra="forbid" turns unknown fields into an error rather than silently ignoring them, which catches client mistakes and makes probing visible. And converting a validated model to an entity should be explicit field assignment or a controlled mapping, not a wholesale dict expansion.

71

How do you handle secrets in a FastAPI application?

Read them from the environment or a secret manager, never from source control. Pydantic Settings with SecretStr is the practical approach: the type prevents accidental exposure because its repr and str are redacted, so logging the settings object or including it in an error does not leak the value. You call get_secret_value() explicitly where you need it, which makes the access visible. In deployment, secrets should come from the platform — Kubernetes secrets mounted as files, AWS Secrets Manager, Vault — rather than plain environment variables where possible, since environment variables are visible in process listings and often end up in crash dumps and logs. Pydantic Settings supports reading from a secrets directory, which is the file-mount pattern. The operational practices that matter as much: rotate credentials on a schedule and have a tested rotation procedure; scan the repository for committed secrets with a pre-commit hook and in CI; and treat any secret that has ever been committed as compromised and rotate it, because removing it from history does not undo the exposure. And validate secrets are present at startup, so a missing one fails deployment rather than the first request.

72

What should you never include in an error response?

Stack traces, exception messages from lower layers, SQL fragments, file paths, internal hostnames, library versions, and configuration values. Those reveal your architecture and dependencies, and specific error text has been used to fingerprint vulnerable versions. The subtler leaks. Differential messages in authentication — "user not found" versus "wrong password" — enable username enumeration. Both should be the same generic failure. Differential status codes in authorisation confirm resource existence, which is why 404 is sometimes preferable to 403. And validation errors echoing the submitted value can put a password or a token into a response and into logs, which is why customising the validation error handler to strip the input is worth doing. The correct pattern is to log the full detail server-side against a correlation ID, and return that identifier to the client. A developer reporting "request abc123 failed" lets you find everything; the internet gets nothing useful. For unhandled exceptions specifically, always return a generic message rather than the exception text, since that is where uncontrolled detail escapes — and make sure debug mode is off in production.

73

How does exception handling work in FastAPI?

HTTPException is the built-in way to return an error status from a handler or a dependency — raise it with a status code and detail, and FastAPI produces the response. For everything else, register handlers with @app.exception_handler for specific exception types, which lets you map domain exceptions to HTTP responses in one place. That mapping is the important architectural point. Your service layer should raise domain exceptions — OrderNotFound, InsufficientStock — rather than HTTPException, because a service raising HTTP errors is coupled to the web layer and cannot be reused from a worker or a CLI. The handler translates them at the boundary. A handler for Exception catches anything unhandled and returns a generic 500, which is where you ensure no stack trace escapes. RequestValidationError has its own handler, which is how you reshape 422 responses to match your error format. The detail worth knowing: exception handlers registered for a parent class catch subclasses, so a single handler for a base AppError covers your whole domain hierarchy, with more specific handlers taking precedence.

74

How should you structure error responses?

Consistently, with a machine-readable code and a human-readable message. FastAPI's default is a JSON object with a detail field, which is fine for simple cases but thin — a string tells the client nothing it can branch on. A better shape includes a stable error code such as "insufficient_funds" that clients can switch on without parsing prose, a message, field-level details where applicable, and a correlation identifier for support. RFC 7807 problem+json is the standard format and worth adopting, since some clients already understand it. The consistency requirement matters as much as the content. An API where validation errors, authentication errors and server errors have three different shapes forces clients to write three parsers. Overriding the validation error handler to match your envelope is what makes it uniform. The status code must still be correct, because intermediaries, monitoring and client libraries all read it — a 200 with an error body defeats all of them. And document the error responses in OpenAPI with the responses parameter, since error handling is where integrators spend their time and it is almost always undocumented.

75

What is the difference between returning a Response and returning data?

Returning data — a dict, a model, a list — lets FastAPI validate it against the response model, serialise it, and set the status and headers. Returning a Response object bypasses that. The content is sent as-is, so no response model validation or filtering happens, and you control the status, headers and media type directly. The cases for returning a Response: setting custom headers or cookies; returning a non-JSON body; streaming; and returning pre-serialised content for performance, since skipping validation on a very large payload is measurable. The risk of bypassing is losing the filtering guarantee — the response model is what prevents accidentally serialising fields you did not intend, so returning a raw Response puts that responsibility back on you. The middle ground is declaring a response parameter in the handler, which lets you set headers and cookies while still returning data normally. That is usually the right approach for adding a header, rather than constructing a full Response. And ORJSONResponse as the response class gives faster serialisation with the same semantics, which is a cheap improvement for high-throughput endpoints.

76

How do you set the status code for a response?

The declarative way is status_code on the route decorator, which sets the default for successful responses and documents it in OpenAPI. A creation endpoint should declare 201, and one returning no content should declare 204. The default is 200, which is wrong for creation and for no-content responses and is frequently left unchanged. For a status that varies by outcome — 200 for an update, 201 when the operation created something — declare a response parameter in the handler and set response.status_code on it. That keeps the declarative default while allowing the exception. For errors, raising HTTPException with the status is the idiomatic route. The details worth knowing: a 204 response must have no body, and returning data with it produces an invalid response — FastAPI handles this but the response model should be None. And a 201 should carry a Location header pointing at the created resource, which is easy to omit and is part of doing creation properly. Documenting the non-default statuses an endpoint can return, via the responses parameter, is what makes the API usable.

77

How do you handle a domain exception cleanly?

Define an exception hierarchy for your domain with a common base, raise those from the service layer, and register exception handlers that map them to HTTP responses at the application boundary. The reason not to raise HTTPException from a service is reuse and testability. A service raising HTTP errors cannot be called from a background worker, a scheduled job or a CLI without dragging in the web framework, and testing it means asserting on status codes rather than on domain outcomes. The mapping layer is small: a handler per exception type, or one handler for the base class that reads a status attribute from the exception. The practical shape is a base AppError carrying a code and a message, with subclasses like NotFoundError and ConflictError that imply a status. A single registered handler then covers everything, and adding a new domain error requires no new handler. That also gives you one place to attach the correlation ID and to decide what detail is safe to expose. The alternative some codebases use — returning result objects rather than raising — is defensible but tends to be verbose in Python, where exceptions are idiomatic for exceptional conditions.

78

How do you add a correlation ID to every request and response?

Middleware that reads an incoming request ID header if present, generates one if not, stores it in a context variable, and adds it to the response headers. A contextvars.ContextVar is the right storage because it is per-task and works correctly with async concurrency — unlike a global or a thread-local, which would be shared across concurrent requests on the same thread. The logging configuration then reads that context variable and includes the ID in every log line, which is what makes the whole thing worthwhile: every line produced while handling a request is tagged, so you can retrieve the complete story of one request from a log query. Accepting an incoming ID matters for distributed tracing — the caller's ID propagates, so a chain across services shares one identifier. Using the W3C traceparent header rather than a custom one means standard tracing tools understand it automatically. Outbound calls must forward it, or the chain breaks at the first hop. And include the ID in error responses so a user reporting a failure gives you the exact key to search — which turns an hour of log archaeology into a single query.

79

How should logging be set up in a FastAPI service?

Structured JSON logs to stdout, with the level configurable, and a correlation ID on every line. Stdout because in a container the platform collects it; writing to files inside a container is an anti-pattern. JSON because logs are queried by machines. Free text requires parsing rules that break, whereas structured fields can be filtered and aggregated directly. The configuration needs to intercept Uvicorn's own loggers, or you get two formats in one stream — Uvicorn's access log in its own format alongside your application logs. Either disable Uvicorn's access log and implement your own middleware, or configure its loggers to use your formatter. What to log per request: method, path, status, duration, and the correlation ID. Not the body by default, since that is volume and often personal data. Use the level meaningfully so consumers can filter, and use logger.exception inside except blocks so tracebacks are captured. And avoid f-strings in log calls — pass arguments so formatting is skipped when the level is disabled. Health check requests should be excluded, or they dominate the volume.

80

What should a health check endpoint do?

Distinguish liveness from readiness, because they answer different questions and conflating them causes outages. Liveness asks whether the process is broken and should be restarted. It should check almost nothing — that the process responds. If liveness checks the database and the database has a brief problem, every instance fails liveness and the orchestrator restarts the whole fleet, turning a blip into a full outage. Readiness asks whether this instance should receive traffic. It may check dependencies, because removing an instance from rotation is reversible and harmless. So readiness can verify the database pool and any critical dependency; liveness should not. The implementation details: keep it cheap, since it runs constantly across every instance. A full query per check adds real load. Exclude health checks from access logs and latency metrics, or they skew everything. And fail readiness during graceful shutdown before the process stops accepting connections, so traffic drains before it goes away — which is what makes a rolling deployment invisible. A startup probe is worth having separately for a service with slow initialisation.

81

How do you return a file or a large download?

FileResponse for a file on disk, which handles the content type, content length and range requests. StreamingResponse for content generated on the fly. The critical consideration is not passing the bytes through your application at all when you can avoid it. For files in object storage, a pre-signed URL redirect or returned link means the client downloads directly from storage — no bandwidth through your service, no memory, no worker held for the duration of a slow download. That is the right architecture for anything large, and it is what people often miss. When you must stream through the service, use a generator so memory stays constant, and be aware that the generator runs after dependency teardown — so a database session closed by a dependency is unavailable inside it. Set Content-Disposition to control the filename and whether it renders inline or downloads, and sanitise that filename since it comes from data. And remember a buffering reverse proxy defeats streaming: nginx buffers by default, so the client sees nothing until the whole response is ready. Range request support matters for resumable downloads and media seeking.

82

How do you document error responses in OpenAPI?

The responses parameter on the route decorator, mapping status codes to a description and optionally a model. That is what turns an OpenAPI schema from a description of the happy path into something an integrator can actually build against. Error responses are the part almost always missing, and they are where client developers spend their time. Declaring a model for the error shape means generated clients get a typed error rather than an untyped blob. For errors that apply across many routes — 401, 429, 500 — declaring them per route is repetitive. You can set them at the router level with the responses argument on APIRouter or include_router, which applies to every route in the group. The practical value beyond documentation: it forces you to enumerate what an endpoint can return, which often reveals that the error contract is inconsistent between endpoints. Examples attached to the error responses make the docs considerably more useful than a bare schema, since an integrator can see the actual shape and the codes. And keeping the declared errors accurate is easier if there is one shared error model rather than ad hoc shapes.

83

How do you test a FastAPI application?

TestClient, which wraps httpx and calls the application in-process — no server, no network, so tests are fast. It is a synchronous interface even for async applications, because it runs the event loop internally. That is convenient: most tests can be plain functions. For tests that need to await things themselves — checking database state with an async session, or testing concurrent behaviour — use httpx.AsyncClient with ASGITransport instead, which requires the test to be async. The pattern that makes it work well is dependency_overrides. Replace the database session with one bound to a test database or a rolled-back transaction; replace the current-user dependency so tests do not construct real tokens; replace external clients with fakes. That lets you test the real routing, validation and serialisation against controlled dependencies, which is the right level for most tests. The things to test at this level: status codes, response shapes, validation failures, and authorisation. Business logic belongs in unit tests of the service layer, which need no HTTP at all. And clear overrides between tests, or state leaks and tests become order-dependent.

84

How do you test with a real database?

Testcontainers, starting a real PostgreSQL in Docker for the test session. SQLite is tempting because it is fast and needs no infrastructure, but it behaves differently enough to give false confidence: different SQL dialect, no real concurrency, different constraint and type behaviour. Tests pass and production fails. The structure that works: a session-scoped fixture starting the container and running migrations once, and a function-scoped fixture giving each test a transaction that is rolled back afterwards. That gives fast per-test isolation without recreating the schema. The rollback approach has a caveat — code under test that commits will break the outer transaction, so either the session is bound to the transaction with a savepoint, or you truncate tables between tests instead. Override the session dependency so the application uses the test session, which is what connects the two halves. For async, the fixtures must be async and their event loop scope must match, or you get "attached to a different loop" errors that are hard to interpret. And run migrations rather than creating tables from metadata, so the migrations themselves are tested.

85

How do you test authentication-protected endpoints?

Override the authentication dependency to return a fixed user, which is the cleanest approach for most tests. That avoids constructing real tokens, avoids coupling every test to the token format, and makes it trivial to test as different users or roles by parameterising the override. The alternative is generating a real token in a fixture and sending it in the header, which exercises the full authentication path. That is worth having for a small number of tests specifically covering authentication — token expiry, invalid signature, missing header — but using it everywhere is slow and brittle. So the split is: a few tests exercising real authentication, and everything else using the override. The authorisation tests that matter and are often missed: accessing another user's resource should fail; an endpoint requiring a role should reject a user without it; and an unauthenticated request should return 401 rather than 403 or 500. A useful discipline is a test that enumerates every route and asserts that each one either is explicitly public or requires authentication — which catches the endpoint someone added without a dependency, and that is exactly the failure that reaches production.

86

How do you mock external HTTP calls in tests?

respx is the natural fit when using httpx, since it intercepts at the transport layer and lets you assert on the requests made as well as returning canned responses. The alternative is overriding the dependency that provides the HTTP client with a fake implementation, which is often better because it tests against your own interface rather than against the wire format — and it means the test does not break when you change client libraries. What to assert: not only that the call was made, but that it was made with the right payload and headers. A test that only checks the response has not verified the integration. The failure cases deserve tests as much as the happy path: a timeout, a 500 from the dependency, and a malformed response body. Those paths are where the bugs are, and they are almost never covered. The higher-fidelity option is a contract test or a recorded interaction, which catches the case where the real API changed and your fake did not — the fundamental weakness of all mocking. And never let tests make real network calls, since they become slow, flaky and dependent on someone else's uptime.

87

What should you test at the API level versus the service level?

At the API level: routing, request parsing and validation, serialisation and response shape, status codes, authentication and authorisation, and error mapping. Those are properties of the HTTP boundary and can only be verified through it. At the service level: business logic, edge cases, and the combinations of conditions that produce different outcomes. Those need no HTTP, run in microseconds, and are far easier to write exhaustively. The common mistake is testing business logic through the API. It works, but each test is slower, requires constructing a request and parsing a response, and couples the test to the endpoint's shape — so a URL change breaks tests about pricing rules. The inverse mistake is testing only the service layer and never the wiring, so the endpoint returns 500 because a response model does not match what the service returns. The practical distribution is many fast service-level tests covering the logic thoroughly, and a smaller number of API tests covering each endpoint's contract — a happy path, a validation failure, an authorisation failure. That keeps the suite fast enough to run constantly, which is what determines whether it gets run.

88

How do you test WebSocket endpoints?

TestClient provides websocket_connect as a context manager, giving an object you can send to and receive from synchronously. That covers the basics: connect, send a message, assert on the response, and verify the connection closes cleanly. What is harder and more valuable to test: disconnection handling, since a client going away mid-stream is the common real-world case and a handler that does not clean up leaks a connection registration; authentication at connect time, including rejection of an invalid token; and message validation, since a malformed message should not kill the connection. Broadcast behaviour needs several connections, which the context manager supports by nesting. The part that is genuinely difficult to test is multi-worker behaviour, because the whole point of a shared pub/sub layer is that connections live on different processes — and an in-process test cannot exercise that. The realistic approach is testing the pub/sub integration separately and accepting that the distributed case needs an integration environment. And timing-dependent tests should poll for a condition with a timeout rather than sleeping, or they become flaky.

89

How do you test that validation works as intended?

Parametrised tests over invalid inputs, asserting both the status and which field failed. Asserting only that the status is 422 is weak — it passes whether the right field was rejected or a different one. Checking the error location confirms the validation you meant is the one that fired. The cases worth covering: missing required fields, wrong types, values outside declared constraints, empty strings where a minimum length is set, and extra fields if you use extra="forbid". The boundary values matter most — exactly at the minimum and maximum, and one either side — since off-by-one errors in constraints are common. For models with cross-field validators, test the combinations that should fail together. Hypothesis is worth considering here: property-based testing generates inputs and finds edge cases that hand-written parameters miss, and it shrinks a failure to a minimal example. The test that is easy to forget is the positive one — that a valid payload at the boundary is accepted — because a constraint that is too strict rejects legitimate input, and only a test with realistic data catches it.

90

How do you keep a test suite fast as the application grows?

Separate fast unit tests from slower integration tests with markers, so the fast set runs constantly during development and the full set runs in CI. A suite taking ten minutes stops being run before committing. Get fixture scope right. Starting a database container per test is enormously wasteful; a session-scoped container with per-test transaction rollback gives isolation at a fraction of the cost. Push logic tests down to the service layer where they need no HTTP and no database. Run in parallel with pytest-xdist, which requires tests to be independent — a requirement they should meet anyway, and parallelism exposes hidden shared state, which is a useful side effect. Avoid sleeps. Waiting a fixed duration for something async is both slow and flaky; poll for the condition with a timeout. Profile with --durations to find the worst offenders, since the time is usually concentrated in a handful of tests rather than spread evenly. And watch for expensive module-level work at import, which is paid on every run regardless of which tests you select.

91

What is contract testing and does FastAPI help?

Contract testing verifies that a provider still satisfies what its consumers actually depend on, rather than testing against a hypothetical specification. FastAPI helps by generating an accurate OpenAPI schema from the implementation, which cannot drift from the code. That gives a machine-readable contract for free. The practical technique that follows: commit the generated schema and diff it in CI. A change classified as breaking — a removed field, a tightened constraint, a changed type — fails the build. That catches accidental breakage at the commit that caused it rather than in a consumer's production. Schemathesis takes it further, generating test cases from the schema and fuzzing the endpoints, which finds cases where the implementation does not honour its own declared contract — a surprisingly common finding. For consumer-driven contracts, Pact lets each consumer publish the subset it uses and the provider verifies against all of them. That is stronger than schema diffing because it tells you which consumer would break, but it requires the consumers to participate. The schema diff in CI is the cheap version and worth doing on any API with external consumers.

92

How do you test background tasks and async side effects?

TestClient runs background tasks synchronously before returning from the request, which is convenient — assertions after the call see their effects. That differs from production, where they run after the response is sent, so a test passing does not prove the ordering is safe. For tasks pushed to a real queue, the cleaner approach is to override the dependency that enqueues, replacing it with a fake that records what was enqueued. Then assert on the recorded calls: the right task with the right arguments. That tests your side of the contract without needing a broker. The worker itself should be tested separately as a plain function, with no queue involved. For async side effects generally, the flaky pattern to avoid is sleeping and hoping. Poll for the condition with a timeout, or use an event the test can wait on. And test the failure paths: what happens when the task raises. With BackgroundTasks the exception is largely invisible, which is one of the arguments against using it for anything important — and a test that demonstrates the silence is a useful thing to have written.

93

How do you deploy a FastAPI application?

An ASGI server — Uvicorn — in a container, with a process manager for multiple workers. The common setups: Gunicorn with Uvicorn workers, which gives process supervision and restarts; or plain Uvicorn with --workers; or one Uvicorn process per container with the orchestrator handling replication, which is the cleanest model in Kubernetes because scaling and restarts are the platform's job. Behind it, a reverse proxy — nginx or the cloud load balancer — terminating TLS, enforcing request size limits and timeouts, and serving static files. The things that matter operationally: worker count roughly matching cores, remembering that each worker has its own connection pools; graceful shutdown so in-flight requests complete during a rolling deployment, which means the termination grace period must exceed your longest request; and a readiness probe that fails before shutdown begins so traffic drains first. Run as a non-root user, use a slim base image, and install dependencies in a separate layer for caching. And do not use --reload in production, which is a development convenience with real overhead.

94

How many workers should you run and why?

Roughly one per core as a starting point, then measure. The reasoning: async gives concurrency within a process but a single process uses one core, so parallelism requires multiple processes. For CPU-bound work, more workers than cores adds context switching without throughput. For I/O-bound async work, the loop already handles concurrency, so extra workers beyond core count mainly add memory. The constraints that usually bind before CPU. Memory: each worker is a full copy of the application, so a large model or cache multiplies. Database connections: pool size times worker count is the real number against the database, and exceeding its limit is a common outage — a pool of twenty with eight workers is a hundred and sixty connections. So worker count is frequently limited by the database rather than by cores. In a container orchestrator, one worker per container is often better than several, because the platform handles scaling and restart, resource limits apply per container, and a crash takes out one worker rather than several. And remember CPU limits: a container limited to one CPU should not run eight workers.

95

What should you monitor in a FastAPI service?

The RED metrics per endpoint: request rate, error rate, and duration at percentiles rather than as an average — p50, p95 and p99, because the mean hides the tail and the tail is what users experience. Per endpoint matters, since an aggregate is dominated by whichever route has the most traffic and a slow important endpoint is invisible. Beyond that: database connection pool utilisation and wait time, which is usually the first thing to saturate; the thread pool for sync handlers, which can exhaust silently; outbound call latency and error rate per dependency; and queue depth if you have one. Event loop lag is worth measuring specifically in an async service — it directly reveals blocking, and nothing else does. For tracing, OpenTelemetry with the FastAPI instrumentation gives per-request spans including database and outbound calls, which is what turns "this endpoint is slow" into "this query is slow". Exclude health checks from the metrics, or they dominate and skew the percentiles toward zero. And alert on the things users notice — error rate and p99 latency — rather than on resource utilisation.

96

What are the common causes of a slow FastAPI endpoint?

In rough order of frequency. The database: a missing index, an N+1 from lazy-loaded relationships, or a query returning far more than needed. This is the usual answer and the first place to look. Blocking the event loop with a synchronous call inside an async handler, which makes latency scale with concurrency rather than staying flat. Sequential awaits where concurrent ones would do, so three dependencies take the sum rather than the maximum. A slow outbound dependency with no timeout, holding the request. Serialising a very large response, where the response model validation and JSON encoding become significant — visible as time spent after the handler returns. Connection pool exhaustion, where the time is spent waiting for a connection rather than executing. Thread pool exhaustion for sync handlers, with the same signature. And middleware doing per-request work, which is easy to overlook because it is not in the handler. The diagnostic order is a trace showing where time goes, then the query plan if it is the database. Guessing is usually wrong, and the instinct to blame the framework almost always is.

97

How do you handle graceful shutdown?

On SIGTERM, the server should stop accepting new connections, finish in-flight requests, run the lifespan shutdown to close pools and clients, and then exit. Uvicorn does this, with a timeout after which remaining connections are dropped. The part that must be coordinated is the orchestrator. Kubernetes sends SIGTERM then waits for the termination grace period before SIGKILL. If the grace period is shorter than your longest request, requests are killed mid-flight on every deployment — which appears to users as intermittent errors during releases. The subtler issue is that removal from the load balancer is not instantaneous. A pod can receive traffic for a short period after SIGTERM, so failing the readiness probe and sleeping briefly before beginning shutdown avoids the errors that otherwise occur at the very start of termination. The lifespan shutdown must actually close things: database pools, HTTP clients, broker connections. Leaving them means connections lingering on the server until they time out, which under frequent deployments accumulates. And background tasks in flight are lost, which is another reason not to use them for work that matters.

98

How do you add caching to a FastAPI service?

Decide what to cache and where, working outward from the client. HTTP caching is the cheapest: set Cache-Control on responses that can be reused, and support conditional requests with ETag so a client can revalidate without transferring the body. For public, non-personalised responses this offloads work entirely. A shared cache — Redis — sits in front of expensive computation or queries. Cache the shaped response rather than the ORM objects, since a deserialised entity is detached and its relationships are broken. In-process caching with lru_cache is fastest but per worker, so it multiplies memory and can serve inconsistent results across workers. Acceptable for immutable reference data, risky otherwise. The hard part is invalidation, so prefer short TTLs and event-driven invalidation over long TTLs with manual purging. The cache key must include everything the response varies by — crucially the user or tenant for anything personalised, or you serve one user's data to another, which is a serious incident and an easy mistake. And guard against stampedes: coalesce concurrent misses, and jitter TTLs so entries do not expire together.

99

What would you check before putting a FastAPI service into production?

Configuration validated at startup, with secrets from a secret manager and nothing in the repository. Docs disabled or protected, since they publish the whole API surface. CORS configured with explicit origins, not a wildcard. Every endpoint's authentication and authorisation verified — ideally by a test that enumerates routes and asserts each is deliberately public or protected. Timeouts on every outbound call and a request timeout at the proxy, with the layers ordered so inner timeouts are shorter. Connection pool sized against the database limit, accounting for worker count. Structured logging to stdout with correlation IDs, and no secrets or stack traces in responses. Health checks split into liveness and readiness, with readiness failing before shutdown. Graceful shutdown tested against the orchestrator's grace period. Metrics and tracing wired up, with alerts on error rate and p99 latency. Rate limiting on anything public. Migrations run as a separate step rather than at startup, so several replicas do not race. And a load test at expected peak, because most of these only fail under concurrency.

100

When is FastAPI the wrong choice?

When you need a full application framework rather than an API. Django gives an ORM, migrations, an admin interface, authentication, forms and a large ecosystem out of the box. For a conventional application with a database and internal users, the Django admin alone can save weeks, and FastAPI has no equivalent. When the team has no async experience and the service does not need it. Async introduces failure modes — blocking the loop, cancellation, event loop lag — that a synchronous stack does not have, and the performance benefit is often negligible for a database-bound CRUD service. When the dominant libraries for your domain are synchronous, so you would be wrapping everything in threads anyway. When you are rendering server-side HTML, where a framework with mature templating and form handling fits better. And for very small services, where the framework choice matters less than the deployment story. The honest positioning: FastAPI is excellent for APIs, particularly where the OpenAPI schema matters to consumers, where validation is substantial, or where concurrency is genuinely high. Those are common, which is why it has grown — but they are not universal.

Learn this free with Aria, your AI tutor → AiCanCode.org/learn/interview