0% found this document useful (0 votes)
4 views22 pages

Fastapi Module3

This document provides comprehensive technical documentation for building APIs using FastAPI, covering routes, CRUD operations, validation, and asynchronous programming. It includes detailed explanations, code examples, and diagrams to illustrate concepts such as returning responses, handling validations, and implementing asynchronous operations. The content is structured into sections that guide the reader through the process of creating a fully functional API with FastAPI.

Uploaded by

murshedjamilalif
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views22 pages

Fastapi Module3

This document provides comprehensive technical documentation for building APIs using FastAPI, covering routes, CRUD operations, validation, and asynchronous programming. It includes detailed explanations, code examples, and diagrams to illustrate concepts such as returning responses, handling validations, and implementing asynchronous operations. The content is structured into sections that guide the reader through the process of creating a fully functional API with FastAPI.

Uploaded by

murshedjamilalif
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Building APIs with FastAPI

Module 3  Complete Technical Documentation


Routes, CRUD, Validation, and Async

Comprehensive notes with code, diagrams, and deep explanations

Compiled from Module 3 study notes

May 12, 2026


FastAPI  Module 3 CONTENTS

Contents
Code Files in This Module 2

1 Creating APIs using FastAPI 3


1.1 Routes and Endpoints  The Mental Model . . . . . . . . . . . . . . . . . . . . . 3
1.2 Returning Responses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.1 Returning a Dictionary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.2 Returning a Pydantic Model . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.3 Returning Custom Response Types . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Returning Pydantic Models  What FastAPI Does Under the Hood . . . . . . . 5
1.4 HTTP Methods in FastAPI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

2 CRUD Operations  Employees API 6


2.1 Resource Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Two Files, One API  Why Separate the Schema . . . . . . . . . . . . . . . . . . 7
2.3 Step 1  The Plain Schema ([Link]) . . . . . . . . . . . . . . . . . . . . . . 7
2.4 Step 2  The Validated Schema (models_val.py) . . . . . . . . . . . . . . . . . 7
2.5 Step 3  The Routes ([Link]) . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.6 Walking Through Each Operation . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.6.1 Read All  GET /employees . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.6.2 Read One  GET /employees/{emp_id} . . . . . . . . . . . . . . . . . . . 9
2.6.3 Create  POST /add_employee . . . . . . . . . . . . . . . . . . . . . . . . 10
2.6.4 Update  PUT /update_employee/{emp_id} . . . . . . . . . . . . . . . . 10
2.6.5 Delete  DELETE /delete_employee/{emp_id} . . . . . . . . . . . . . . . 10

3 Handling Validations and Errors 11


3.1 Field Validation with Pydantic . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.2 Optional Fields and Default Values . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.3 Custom Error Responses with HTTPException . . . . . . . . . . . . . . . . . . . . 13
3.4 Putting It All Together  A Validated Endpoint . . . . . . . . . . . . . . . . . . 14

4 Asynchronous Programming 15
4.1 What Is Asynchronous Programming? . . . . . . . . . . . . . . . . . . . . . . . . 15
4.2 Synchronous vs Asynchronous  Visual . . . . . . . . . . . . . . . . . . . . . . . 15
4.3 Sync vs Async  By Operation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
4.4 The Sync vs Async Demo  Measured . . . . . . . . . . . . . . . . . . . . . . . . 15
4.4.1 Synchronous Version  [Link] . . . . . . . . . . . . . . . . . . . . 16
4.4.2 Asynchronous Version  [Link] . . . . . . . . . . . . . . . . . . 16
4.4.3 Decoding the Async Mechanics . . . . . . . . . . . . . . . . . . . . . . . . 17
4.5 async and await  The Keywords . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.6 Internal Working  The Event Loop . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.7 Blocking vs Non-Blocking Sleep . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.8 From Demos to FastAPI  async_main.py . . . . . . . . . . . . . . . . . . . . . 18
4.9 When to Use async def vs def in FastAPI . . . . . . . . . . . . . . . . . . . . . 19
4.10 A Concrete Async Win  Calling Two APIs in Parallel . . . . . . . . . . . . . . 20

5 Summary  One-Page Mental Model 21

1
FastAPI  Module 3 CONTENTS

Code Files in This Module


This module is built around eight Python les that you can nd in the 3. Building APIs
folder. They are not independent toys  they form a progression, and several of them import
from each other. Before diving in, here is the map.

File Teaches What it does


[Link] Ÿ1 The simplest possible FastAPI app  one route, one dict
response
[Link] Ÿ1 Returning a Pydantic model with response_model
[Link] Ÿ2 Plain Employee schema  no validation rules
models_val.py Ÿ23 Validated Employee schema with Field, StrictInt,
Optional
[Link] Ÿ23 The full CRUD API  imports Employee from
models_val.py
[Link] Ÿ4 Three tasks run sequentially with [Link]
[Link] Ÿ4 Same three tasks run concurrently with [Link]
async_main.py Ÿ4 A FastAPI route declared with async def and await

How the Files Connect


Ÿ1  First routes

[Link] [Link]

Ÿ23  CRUD + validation

[Link] evolves into models_val.py imports [Link]

Ÿ4  Asynchronous programming

[Link] compare timing [Link] used in FastAPI async_main.py

Read the diagram this way: the solid teal arrow is a real Python import statement ([Link]
has from models_val import Employee); the dashed grey arrows show conceptual evolution
(one le is the next version of the previous one); the orange dashed double-arrow shows two les
meant to be compared side-by-side.

2
FastAPI  Module 3 1 CREATING APIS USING FASTAPI

1 Creating APIs using FastAPI


1.1 Routes and Endpoints  The Mental Model
A route (or endpoint) is a specic URL path that a client can reach out to. In FastAPI,
every route is bound to a Python function called a path operation function. When an HTTP
request arrives whose method and URL match a route, that function is executed.

Three things conspire to dene a route:

1. An HTTP method (GET, POST, PUT, DELETE, . . . ).

2. A URL path (e.g. /users, /items/{id}).


3. A path operation function that runs when the method and path are matched.

Listing 1: [Link]  the anatomy of a route



1 from fastapi import FastAPI
2
3 app = FastAPI ()
4
5
6 @app . get ( '/ ')
7 def home () :
8 return { ' message ': ' Hello FastAPI ! '}

Run this with:



1 uvicorn basic - app : app -- reload

Open [Link] and the browser shows {"message":"Hello FastAPI!"}.

Component Role

@[Link]("/") Decorator that registers a GET route at the root URL.

home() The function that executes when this route is accessed.

return {...} The return value is automatically converted to a JSON


response.

Deep Dive

Why path operation function and not just handler? In FastAPI's vocabulary,
the term emphasises that one function corresponds to one path + one method. The same
path /items/{id} can have several operations : GET (read), PUT (replace), DELETE (remove)
 each a separate path operation function. This vocabulary maps directly onto the OpenAPI
specication, where each (path, method) pair is called an operation.

1.2 Returning Responses


FastAPI accepts three families of return values from your path operation function. Each is
serialized dierently.

3
FastAPI  Module 3 1 CREATING APIS USING FASTAPI

1.2.1 Returning a Dictionary

The simplest case: return a Python dict, and FastAPI calls [Link] on it (technically,
jsonable_encoder rst, which handles dates, UUIDs, Pydantic objects, etc.).

Listing 2: Dict response  simplest possible



1 @app . get ( " / ping " )
2 def ping () :
3 return { " status " : " ok " , " uptime_seconds " : 142}

This returns:

1 { " status " : " ok " , " uptime_seconds " : 142}

1.2.2 Returning a Pydantic Model

Returning a Pydantic instance gives you the same JSON output plus automatic validation that
the response matches your declared schema. This is the recommended pattern.

Listing 3: [Link]  returning a Pydantic model



1 from fastapi import FastAPI
2 from pydantic import BaseModel
3
4
5 class User ( BaseModel ) :
6 id : int
7 name : str
8
9
10 app = FastAPI ()
11
12
13 @app . get ( '/ user ' , response_model = User )
14 def get_user () :
15 return User ( id =1 , name = ' Bruce ')

When a client hits GET /user, FastAPI:

1. Calls get_user(), which constructs a User instance.

2. Validates that the returned object matches the response_model (here, the same class, so it
always passes).

3. Serializes it to JSON: {"id":1,"name":"Bruce"}.


4. Adds an entry to the OpenAPI spec at /[Link] describing this exact schema.

1.2.3 Returning Custom Response Types

For non-JSON output, FastAPI provides specialised response classes:

Listing 4: HTML, plain text, and streaming



1 from fastapi import FastAPI

4
FastAPI  Module 3 1 CREATING APIS USING FASTAPI

2 from fastapi . responses import HTMLResponse , PlainTextResponse ,


StreamingResponse
3
4 app = FastAPI ()
5
6 @app . get ( " / page " , response_class = HTMLResponse )
7 def page () :
8 return " <h1 > Hello , browser ! </ h1 > "
9
10 @app . get ( " / text " , response_class = PlainTextResponse )
11 def text () :
12 return " raw text , no quotes around it "
13
14 @app . get ( " / stream " )
15 def stream () :
16 def gen () :
17 for i in range (5) :
18 yield f " chunk { i }\ n "
19 return StreamingResponse ( gen () , media_type = " text / plain " )

1.3 Returning Pydantic Models  What FastAPI Does Under the Hood
When you write the route in [Link] withresponse_model=User, you might think
the keyword is just decoration. In fact, the decorator is syntactic sugar for a more verbose
registration call.

What you write:



1 @app . get ( '/ user ' , response_model = User )
2 def get_user () :
3 return User ( id =1 , name = ' Bruce ')

What FastAPI actually executes behind the scenes:



1 app . add_api_route (
2 path = '/ user ' ,
3 endpoint = get_user ,
4 methods =[ ' GET '] ,
5 response_model = User ,
6 )

The add_api_route call wires four things together:

1. Inserts /user into Starlette's URL router with the methods list ["GET"].
2. Registers get_user as the callable to invoke.

3. Sets User as the response model, which (a) lters the returned dict so only elds declared on
User survive, (b) re-validates the output, (c) generates the JSON schema entry in OpenAPI.
4. Adds the route to the OpenAPI 3.1 spec at /[Link].
Note

Practical consequence of response_model. If your function accidentally returns extra


elds (a leaked password hash, internal IDs), response_model strips them. Always use a
separate response model when serving data that contains sensitive elds. Input model

5
FastAPI  Module 3 2 CRUD OPERATIONS  EMPLOYEES API

and output model should rarely be the same class.

1.4 HTTP Methods in FastAPI


FastAPI exposes a decorator for every standard HTTP verb. Each has a precise semantic
meaning in REST: not just what does it do, but what guarantees does the client get.

Method Purpose Syntax Idempotent?

GET Retrieve data @[Link]() Yes

POST Create new data @[Link]() No

PUT Update / replace data @[Link]() Yes

PATCH Partial update @[Link]() Sometimes

DELETE Delete data @[Link]() Yes

Deep Dive

Idempotent means: calling the same request multiple times produces the same server
state as calling it once. DELETE /items/5 called ve times leaves the same end state as one
call  the item is gone. POST /items called ve times creates ve dierent items, which
is why it is not idempotent. This matters in production: clients (and CDNs, and proxies)
may retry idempotent requests automatically on network failure; they will refuse to retry
POSTs to avoid duplicate creation.

Listing 5: All ve verbs on the same resource



1 @app . get ( " / items " ) # list all
2 def list_items () : ...
3
4 @app . get ( " / items /{ id } ") # read one
5 def get_item ( id : int ) : ...
6
7 @app . post ( " / items " ) # create one ( server assigns id )
8 def create_item ( item : ItemIn ) : ...
9
10 @app . put ( " / items /{ id } ") # replace entire item
11 def replace_item ( id : int , item : ItemIn ) : ...
12
13 @app . patch ( " / items /{ id } " ) # update some fields
14 def patch_item ( id : int , partial : ItemPatch ) : ...
15
16 @app . delete ( " / items /{ id } " ) # delete one
17 def delete_item ( id : int ) : ...

2 CRUD Operations  Employees API


CRUD (Create, Read, Update, Delete) is the canonical exercise for any web framework. We
will build a complete Employees API that demonstrates every concept.

2.1 Resource Design


Five endpoints map naturally onto CRUD:

6
FastAPI  Module 3 2 CRUD OPERATIONS  EMPLOYEES API

Operation Method Path Description

List all GET /employees Show all employees

Get one GET /employees/{id} Show a specic employee

Create POST /employees Add a new employee

Update PUT /employees/{id} Update an existing employee

Delete DELETE /employees/{id} Delete an employee

2.2 Two Files, One API  Why Separate the Schema


Your code splits the API into two les. This is a small but important architectural choice:

ˆ models_val.py  denes the Employee Pydantic schema. Pure data, no routes, no FastAPI.

ˆ [Link]  denes the routes. Imports Employee from models_val.py.


Why split? Because routes change often (new endpoints, refactored URLs) while data schemas
change rarely. Keeping the schema in its own module means:

1. Other modules (tests, background workers, CLI scripts) can import Employee without drag-
ging in FastAPI.

2. Schema changes are visible in a single, focused le.

3. Routes and data are independently testable.

2.3 Step 1  The Plain Schema ([Link])


Before adding validation, the schema is just types:

Listing 6: [Link]  the unvalidated starting point



1 from pydantic import BaseModel
2
3
4 class Employee ( BaseModel ) :
5 id : int
6 name : str
7 department : str
8 age : int

This already gives us a lot for free:

ˆ Type coercion ("1" →1 for id).


ˆ Automatic 422 errors if required elds are missing.

ˆ JSON Schema generation for OpenAPI.

But it doesn't catch any of these bad inputs: id = -5, name = "X", age = 5. Those would all
pass. We need explicit constraints, which leads us to the validated version.

2.4 Step 2  The Validated Schema (models_val.py)


Listing 7: models_val.py  validation added

1 from pydantic import BaseModel , Field , StrictInt

7
FastAPI  Module 3 2 CRUD OPERATIONS  EMPLOYEES API

2 from typing import Optional


3
4
5 class Employee ( BaseModel ) :
6 id : int = Field (... , gt =0 , title = ' Employee ID ')
7 name : str = Field (... , min_length =3 , max_length =30)
8 department : str = Field (... , min_length =3 , max_length =30)
9 age : Optional [ StrictInt ] = Field ( default = None , ge =21)

Reading the constraints eld by eld:

Field What it enforces


id ... marks it required; gt=0 forbids zero and negatives; title appears in
Swagger UI.
name Required string, length 3 to 30. Rejects "Al" (too short) and giant blobs (too
long).
department Same constraints as name.
age Optional with a default of None. If provided, must be a true integer (StrictInt
 not "21" as a string) and at least 21.

Deep Dive

Why StrictInt only on age, and not on id? Because id is more often passed as a string
from URL paths or form data, where coercion is helpful. age is part of the JSON body,
where a client sending "21" as a string usually indicates a frontend bug worth surfacing.
This is a real design trade-o  coercion is convenience, strictness is correctness, and you
choose per eld.

2.5 Step 3  The Routes ([Link])


Now the FastAPI side. Note line 2: from models_val import Employee. This is the one line
that wires the two les together.

Listing 8: [Link]  the complete CRUD routes



1 from fastapi import FastAPI , HTTPException
2 from models_val import Employee
3 from typing import List
4
5 employees_db : List [ Employee ] = []
6
7 app = FastAPI ()
8
9
10 # 1. Read all employees
11 @app . get ( '/ employees ' , response_model = List [ Employee ])
12 def get_employees () :
13 return employees_db
14
15
16 # 2. Read specific employee
17 @app . get ( '/ employees /{ emp_id } ' , response_model = Employee )
18 def get_employee ( emp_id : int ) :
19 for index , employee in enumerate ( employees_db ) :
20 if employee . id == emp_id :
21 return employees_db [ index ]
22 raise HTTPException ( status_code =404 , detail = ' Employee Not Found ')

8
FastAPI  Module 3 2 CRUD OPERATIONS  EMPLOYEES API

23
24
25 # 3. Add an employee
26 @app . post ( '/ add_employee ' , response_model = Employee )
27 def add_employee ( new_emp : Employee ) :
28 for employee in employees_db :
29 if employee . id == new_emp . id :
30 raise HTTPException ( status_code =400 , detail = ' Employee already
exists ')
31 employees_db . append ( new_emp )
32 return new_emp
33
34
35 # 4. Update an employee
36 @app . put ( '/ update_employee /{ emp_id } ' , response_model = Employee )
37 def update_employee ( emp_id : int , updated_employee : Employee ) :
38 for index , employee in enumerate ( employees_db ) :
39 if employee . id == emp_id :
40 employees_db [ index ] = updated_employee
41 return updated_employee
42 raise HTTPException ( status_code =404 , detail = ' Employee Not Found ')
43
44
45 # 5. Delete an employee
46 @app . delete ( '/ delete_employee /{ emp_id } ')
47 def delete_employee ( emp_id : int ) :
48 for index , employee in enumerate ( employees_db ) :
49 if employee . id == emp_id :
50 del employees_db [ index ]
51 return { ' message ': ' Employee deleted successfully '}
52 raise HTTPException ( status_code =404 , detail = ' Employee Not Found ')

2.6 Walking Through Each Operation


2.6.1 Read All  GET /employees
No parameters. FastAPI invokesget_employees(), which returns the entire employees_db list.
The response_model=List[Employee] declaration validates each element on the way out and
tells Swagger UI to advertise the response as a JSON array of Employee objects.

1 curl http ://[Link]:8000/ employees

2.6.2 Read One  GET /employees/{emp_id}


The path contains a path parameter {emp_id}. FastAPI extracts it, coerces it to int (be-
cause the function signature declares emp_id: int), and passes it in. The function loops
through employees_db; if no match is found, HTTPException(404, ...) aborts the request,
and FastAPI converts the exception into a clean 404 JSON response.

1 curl http ://[Link]:8000/ employees /1
2 curl http ://[Link]:8000/ employees /999 # returns 404

9
FastAPI  Module 3 2 CRUD OPERATIONS  EMPLOYEES API

2.6.3 Create  POST /add_employee


The body arrives as JSON, gets validated against Employee (all the models_val.py constraints
apply  id > 0, name length 330, etc.), and is bound to the parameter new_emp. Before
inserting, the function checks for duplicate IDs; on duplicate, it returns 400 (Bad Request).

1 curl -X POST http ://[Link]:8000/ add_employee \
2 -H " Content - Type : application / json " \
3 -d ' {" id ": 1 , " name ": " Bruce Wayne " , " department ": " R & D " , " age ": 35} '

If you send invalid data, the constraint failure is reported before the function even runs:

Listing 9: What happens with bad input



1 # Request body :
2 # {" id ": 0 , " name ": " Al ", " department ": " HR " , " age ": 18}
3
4 # Response (422) :
5 {
6 " detail " : [
7 { " loc " : [ " body " ," id " ] , " msg " : " Input should be greater than 0 " , ...} ,
8 { " loc " : [ " body " ," name " ] , " msg " : " String should have at least 3
characters " , ...} ,
9 { " loc " : [ " body " ," age " ] , " msg " : " Input should be greater than or equal to
21 " , ...}
10 ]
11 }

2.6.4 Update  PUT /update_employee/{emp_id}


PUT is a complete replacement. The client must send all required elds; whatever was stored
before is overwritten. (For partial updates, you would use PATCH with a separate all-elds-
optional model.)

1 curl -X PUT http ://[Link]:8000/ update_employee /1 \
2 -H " Content - Type : application / json " \
3 -d ' {" id ": 1 , " name ": " Bruce Wayne " , " department ": " Engineering " , " age ":
36} '

2.6.5 Delete  DELETE /delete_employee/{emp_id}


Returns a conrmation message on success, 404 if the ID is unknown.

1 curl -X DELETE http ://[Link]:8000/ delete_employee /1

Note

A note on path style. Your URLs use action verbs in the path: /add_employee,
/update_employee/{id}, /delete_employee/{id}. This works perfectly but is not strictly
RESTful. The REST convention is to use the same resource path (/employees and
/employees/{id}) for all ve operations and let the HTTP method (POST, PUT, DELETE)
communicate the action. Both styles are common in industry; the verb-in-path style is

10
FastAPI  Module 3 3 HANDLING VALIDATIONS AND ERRORS

sometimes preferred when generating client SDKs that map URLs to function names. For
your learning, recognise that the two styles exist and pick consciously.

Deep Dive

In-memory store caveat. employees_db = [] lives in Python memory. Every time


Uvicorn restarts (which reload does on every save), the list is wiped. This is ne for
learning; in production you would replace it with a database (PostgreSQL via SQLAlchemy,
MongoDB via Motor)  the route code largely stays the same, only employees_db operations
change.

3 Handling Validations and Errors


Validation is the line of defence between an API and bad input. FastAPI provides three layers,
in order of sophistication:

1. Type-level validation  the type hint itself (int, str, float).


2. Field-level constraints  using Field() (min/max length, ranges, regex).

3. Object-level rules  using @field_validator or @model_validator for cross-eld logic.

3.1 Field Validation with Pydantic


The Field() helper provides metadata, validation constraints, and default values for elds in
a BaseModel. You already saw it in action in models_val.py (Ÿ2). Here we look at the full
toolkit.

Listing 10: A richer example combining many constraints



1 from pydantic import BaseModel , Field
2
3 class Product ( BaseModel ) :
4 name : str = Field (
5 ... , # ... means required
6 min_length =2 , max_length =100 ,
7 title = " Product name " ,
8 description = " Display name shown to customers " ,
9 example = " Wireless Headphones "
10 )
11 price : float = Field (... , gt =0 , description = " Price in GBP " )
12 stock : int = Field (0 , ge =0) # default 0 , must be >= 0
13 sku : str = Field (... , pattern = r " ^[ A - Z ]{3} -\ d {4} $ " )

11
FastAPI  Module 3 3 HANDLING VALIDATIONS AND ERRORS

Common Parameters of Field

Parameter Description

default Default value, or ... (Ellipsis) to mark the eld as required.

title Human-readable title in OpenAPI / Swagger docs.

description Field description shown in docs.

example Example value displayed in Swagger UI's Try it out.

gt / ge Greater than / greater than or equal (numbers).

lt / le Less than / less than or equal (numbers).

min_length Minimum string length.

max_length Maximum string length.

pattern Regex pattern that the string must match.

Note

In Pydantic v2 the parameter is named pattern, not regex. The older regex keyword
is deprecated; many tutorials still show it. If you upgrade and see a deprecation warning,
change regex= to pattern=.

Strict Types

By default, Pydantic is coercive: a string "5" for an int eld is silently converted to 5. Some-
times you want strict behavior  reject anything that is not literally the right type.

Listing 11: Strict types refuse coercion



1 from pydantic import BaseModel , StrictInt , StrictFloat , StrictStr
2
3 class StrictPayload ( BaseModel ) :
4 count : StrictInt # rejects "5" , accepts only 5
5 rate : StrictFloat # rejects 1 , accepts only 1.0
6 name : StrictStr # rejects 42 , accepts only "42"

Deep Dive

When to use Strict types. In nancial or scientic APIs where confusing 1 (int) with
1.0 (oat) could change the meaning  e.g., share counts vs share prices  strict types make
the contract explicit. For general web APIs, the default coercion is usually what you want.

Back to models_val.py: this is exactly why age uses Optional[StrictInt]  if a client sends
"age": "25" (a string), Pydantic refuses to silently coerce it. A string age is almost always a
frontend bug, and StrictInt surfaces it as a validation error instead of letting it slip through.

3.2 Optional Fields and Default Values


Use Optional[X] (which is equivalent to X | None) to mark a eld as optional. A default value
 either None or a concrete value  must be provided.

Listing 12: Optional elds, three idioms



1 from typing import Optional

12
FastAPI  Module 3 3 HANDLING VALIDATIONS AND ERRORS

2 from pydantic import BaseModel , Field


3
4 class Contact ( BaseModel ) :
5 # 1. Optional with None default
6 middle_name : Optional [ str ] = None
7
8 # 2. Optional with concrete default
9 country : Optional [ str ] = " United Kingdom "
10
11 # 3. Modern syntax ( Python 3.10+)
12 phone : str | None = None

The semantic dierence between Optional[str] = None and str | None is purely syntactic
 both mean a string or None. Use the syntax your team prefers; both are accepted by Pydantic.

3.3 Custom Error Responses with HTTPException


FastAPI's HTTPException is the standard way to abort a request with a specic status code and
message.

Listing 13: Raising structured errors



1 from fastapi import FastAPI , HTTPException , status
2
3 app = FastAPI ()
4
5 @app . get ( " / users /{ user_id } " )
6 def get_user ( user_id : int ) :
7 if user_id < 1:
8 raise HTTPException (
9 status_code = status . HTTP_400_BAD_REQUEST ,
10 detail = " user_id must be a positive integer "
11 )
12 if user_id not in DB :
13 raise HTTPException (
14 status_code = status . HTTP_404_NOT_FOUND ,
15 detail = f " No user with id { user_id } " ,
16 headers ={ "X - Error - Source " : " user - lookup " }
17 )
18 return DB [ user_id ]

What the client receives on a 404:



1 HTTP /1.1 404 Not Found
2 content - type : application / json
3 x - error - source : user - lookup
4
5 { " detail " : " No user with id 17 " }

Common HTTP Status Codes

Use the status module from fastapi for readability:

13
FastAPI  Module 3 3 HANDLING VALIDATIONS AND ERRORS

Code Constant When to use

200 HTTP_200_OK Successful GET, successful PUT/PATCH with


body
201 HTTP_201_CREATED Successful POST that created a resource
204 HTTP_204_NO_CONTENT Successful DELETE; no body
400 HTTP_400_BAD_REQUEST Client error not covered by validation
401 HTTP_401_UNAUTHORIZED Missing or invalid authentication
403 HTTP_403_FORBIDDEN Authenticated but not allowed
404 HTTP_404_NOT_FOUND Resource does not exist
409 HTTP_409_CONFLICT Conict, e.g. duplicate creation
422 HTTP_422_UNPROCESSABLE_ENTITY Validation failure (FastAPI default)
500 HTTP_500_INTERNAL_SERVER_ERRORServer-side bug

3.4 Putting It All Together  A Validated Endpoint


Listing 14: All three validation layers, one endpoint

1 from fastapi import FastAPI , HTTPException , status
2 from pydantic import BaseModel , Field , field_validator
3
4 app = FastAPI ()
5
6 class UserCreate ( BaseModel ) :
7 username : str = Field (... , min_length =3 , max_length =20 ,
8 pattern = r " ^[ a - zA - Z0 -9 _ ]+ $ ")
9 age : int = Field (... , ge =13 , le =120)
10 password : str = Field (... , min_length =8)
11
12 @field_validator ( " username " )
13 @classmethod
14 def no_reserved ( cls , v : str ) -> str :
15 if v . lower () in { " admin " , " root " , " system " }:
16 raise ValueError ( " reserved username " )
17 return v
18
19 _users : set [ str ] = set ()
20
21 @app . post ( " / signup " , status_code = status . HTTP_201_CREATED )
22 def signup ( user : UserCreate ) :
23 # Layers 1 + 2 + 3 have already run by the time we reach here .
24 if user . username in _users :
25 raise HTTPException (
26 status_code = status . HTTP_409_CONFLICT ,
27 detail = " username already taken "
28 )
29 _users . add ( user . username )
30 return { " username " : user . username , " created " : True }

If a client sends {"username": "ab", "age": 12, "password": "123"}, the response is
one 422 with all three errors listed:

1 {
2 " detail " : [
3 { " loc " : [ " body " ," username " ] , " msg " : " String should have at least 3
characters " , ...} ,
4 { " loc " : [ " body " ," age " ] , " msg " : " Input should be >= 13 " , ...} ,

14
FastAPI  Module 3 4 ASYNCHRONOUS PROGRAMMING

5 { " loc " : [ " body " ," password " ] , " msg " : " String should have at least 8
characters " , ...}
6 ]
7 }

Notice: the client gets every error in one round trip. They don't have to x one, retry, x the
next, retry. This single design choice saves your frontend team enormous pain.

4 Asynchronous Programming
4.1 What Is Asynchronous Programming?
Asynchronous programming is a paradigm that lets your program perform other tasks while
waiting for a slow operation (a database query, an HTTP call, a le read) to complete, without
blocking the rest of the code.

In synchronous code, when you call [Link](...), the entire program freezes for the 100 ms it
takes the database to answer. In asynchronous code, you say  await [Link](...)  which
means pause this function here, do other work, and come back when the answer is ready.

4.2 Synchronous vs Asynchronous  Visual


Sync Req A wait for DB Resp A Req B wait for DB Resp B

Async Req A Req B both wait concurrently Resp A Resp B

time
Async: total ≈ longest single wait Sync: ∼total = sum of all waits

In the synchronous timeline, Request B cannot even start until Request A is done. In the
asynchronous timeline, both requests start almost immediately and their database waits overlap.
The total wall-clock time is roughly the longest single wait, not the sum.

4.3 Sync vs Async  By Operation


Operation Synchronous Asynchronous

API call Waits for response before Sends request, does other work,
proceeding returns later

Database query Blocks until data is fetched Queries DB, resumes when data
arrives

File read Waits for disk I/O to com- Reads in background while other
plete code runs

4.4 The Sync vs Async Demo  Measured


To make the abstract timeline concrete, the two demo les run three tasks of length 2 s, 1 s, 3 s
and measure the wall-clock time.

15
FastAPI  Module 3 4 ASYNCHRONOUS PROGRAMMING

4.4.1 Synchronous Version  [Link]

Listing 15: [Link]  three tasks run sequentially



1 import time
2 from timeit import default_timer as timer
3
4
5 def run_task ( name , seconds ) :
6 print ( f '{ name } started at : { timer () } ')
7 time . sleep ( seconds )
8 print ( f '{ name } completed at : { timer () } ')
9
10
11 start = timer ()
12 run_task ( ' Task 1 ' , 2)
13 run_task ( ' Task 2 ' , 1)
14 run_task ( ' Task 3 ' , 3)
15 print ( f '\ nTotal time taken : { timer () - start :.2 f } s ')

Observed output (approximate):



1 Task 1 started at : 0.00
2 Task 1 completed at : 2.00
3 Task 2 started at : 2.00
4 Task 2 completed at : 3.00
5 Task 3 started at : 3.00
6 Task 3 completed at : 6.00
7
8 Total time taken : 6.00 s

Total =2+1+3=6 seconds. Each task blocks until the previous one nishes. [Link] is
blocking
the canonical call  the entire interpreter freezes during it.

4.4.2 Asynchronous Version  [Link]


Same three tasks, same lengths  but launched concurrently with [Link]:

Listing 16: [Link]  three tasks run concurrently



1 import asyncio
2 from timeit import default_timer as timer
3
4
5 async def run_task ( name , seconds ) :
6 print ( f '{ name } started at : { timer () } ')
7 await asyncio . sleep ( seconds )
8 print ( f '{ name } completed at : { timer () } ')
9
10
11 async def main () :
12 start = timer ()
13 await asyncio . gather (
14 run_task ( ' Task 1 ' , 2) ,
15 run_task ( ' Task 2 ' , 1) ,
16 run_task ( ' Task 3 ' , 3)
17 )
18 print ( f '\ nTotal time taken : { timer () - start :.2 f } s ')
19

16
FastAPI  Module 3 4 ASYNCHRONOUS PROGRAMMING

20
21 asyncio . run ( main () )

Observed output:

1 Task 1 started at : 0.00
2 Task 2 started at : 0.00
3 Task 3 started at : 0.00
4 Task 2 completed at : 1.00
5 Task 1 completed at : 2.00
6 Task 3 completed at : 3.00
7
8 Total time taken : 3.00 s

Total = max(2, 1, 3) = 3 seconds  exactly half the synchronous version. Look at the start
times: all three tasks start at 0.00. This is the smoking gun of concurrency. They are not
running on three threads; they are running on a single thread that suspends at each await
[Link](...) and lets the others run.

4.4.3 Decoding the Async Mechanics

1. async def run_task  declares a coroutine. Calling run_task('Task 1', 2) does not
execute the body. It returns a coroutine object.

2. [Link](...)  takes those coroutine objects and submits them all to the event
loop as tasks to run concurrently.

3. await [Link](...)  waits until all submitted tasks complete, then returns their
results.

4. [Link](main())  creates a new event loop, runs main() to completion, and closes the
loop.

Note

Why [Link](main()) at the bottom, but not in FastAPI? In standalone scripts,


you must explicitly start the event loop with [Link]. In FastAPI, Uvicorn already
owns a running event loop  you just write async def on your routes and FastAPI plugs
them in. Calling [Link] from inside a FastAPI route would actually error out, because
you'd be starting a loop inside a loop.

4.5 async and await  The Keywords


To summarise what the two demos showed:

ˆ async def  declares an asynchronous function (called a coroutine). Calling it does not run
it; it returns a coroutine object that must be awaited or scheduled.

ˆ await  pauses execution of the current coroutine until the awaited operation completes, and
yields control back to the event loop so other tasks can run in the meantime.

4.6 Internal Working  The Event Loop


Behind every async program is a single event loop  a scheduler that owns the right to run
coroutines and decides which one runs next.

17
FastAPI  Module 3 4 ASYNCHRONOUS PROGRAMMING

1. Python uses an event loop (provided by asyncio) to manage asynchronous tasks.

2. Coroutines (tasks) are submitted to the loop.

3. When a task hits await, control is yielded back to the loop.

4. The loop runs other ready tasks in the meantime.

5. When the awaited operation nishes, the loop resumes the original task from where it paused.

Task 1
Task 1 yield
result ready
awaiting DB
resume

Task 2 yield Event Picks next


running now Loop ready task

Task 3 yield

awaiting HTTP

The crucial insight: there is only one thread. Concurrency comes not from threads or
processes, but from cooperative yielding at every await. This means async code is free of the
classic locking/race-condition headaches of multithreading  but it also means a single CPU-
heavy task will freeze the entire server until it yields.

4.7 Blocking vs Non-Blocking Sleep


Function Blocking? Description

[Link](3) Yes Pauses the entire thread; nothing else can


run

await No Pauses only this coroutine; other tasks pro-


[Link](3) ceed

4.8 From Demos to FastAPI  async_main.py


The two demo les run in plain Python scripts. The next le, async_main.py, is the smallest
possible FastAPI example using async/await:

Listing 17: async_main.py  async inside a FastAPI route



1 import asyncio
2 from fastapi import FastAPI
3
4 app = FastAPI ()
5
6
7 @app . get ( " / wait " )
8 async def wait () :
9 await asyncio . sleep (3) # Non - blocking sleep
10 return { " message " : " Finished waiting !" }

Why this matters: run this server, then open two browser tabs to [Link]
wait at roughly the same time. Both responses arrive at ≈ 3 s, not 6 s. The second request
didn't wait for the rst to nish  the event loop simply ran them concurrently. If you replaced

18
FastAPI  Module 3 4 ASYNCHRONOUS PROGRAMMING

await [Link](3) with [Link](3), both requests would take 6s total instead of
3 s. This is precisely the warning in the next subsection.
Note

No [Link] here. In the demo scripts you called [Link](main()) to start the
event loop. In async_main.py there is no such call  because Uvicorn already runs the
event loop. You only declare async def and FastAPI plugs it in.

Listing 18: Three sleep patterns side by side  the right one, the wrong one, the alternative

1 import time , asyncio
2 from fastapi import FastAPI
3
4 app = FastAPI ()
5
6 @app . get ( " / sync - block ")
7 def sync_block () :
8 time . sleep (3) # OK : runs in a thread pool
9 return { " finished " : True }
10
11 @app . get ( " / async - good ")
12 async def async_good () :
13 await asyncio . sleep (3) # CORRECT : yields to the event loop
14 return { " finished " : True }
15
16 @app . get ( " / async - bad " )
17 async def async_bad () :
18 time . sleep (3) # BUG : blocks the entire event loop !
19 return { " finished " : True } # nothing else can run for 3 s

Important

The async-bad pattern is the single most common async mistake. The function is declared
async, but it calls a synchronous blocking library ([Link], [Link], psycopg2).
Because async functions run on the event loop, blocking them blocks every other request.
The whole server stops responding for 3 seconds. Rule: if your function is async, every
blocking call inside it must be awaited.

4.9 When to Use async def vs def in FastAPI


FastAPI accepts both. It runs them dierently:

ˆ async def  runs directly on the event loop. Fast, but you must not call blocking libraries.

ˆ def  run in a thread pool (managed by Starlette). Slightly more overhead per request, but
safe to call blocking libraries.

Use async def if your route does:

ˆ HTTP calls with async clients (httpx, aiohttp).


ˆ Database access with async drivers (asyncpg, databases, motor for MongoDB).

ˆ Any I/O via libraries that explicitly support async/await.


ˆ Vector store / LLM API calls (OpenAI, Anthropic, Pinecone all have async clients).

19
FastAPI  Module 3 4 ASYNCHRONOUS PROGRAMMING

Use def if:

ˆ Your function is CPU-bound (ML inference, image processing, heavy NumPy).

ˆ You are calling a blocking library (e.g. requests, psycopg2, sync SDKs).

ˆ You don't have time to rewrite to async and the endpoint isn't on the hot path.

Listing 19: Decision in practice



1 import httpx , requests , joblib
2 from fastapi import FastAPI
3
4 app = FastAPI ()
5 model = joblib . load ( " model . pkl " )
6
7 # Async client + async route = ideal
8 @app . get ( " / proxy - good ")
9 async def proxy_good () :
10 async with httpx . AsyncClient () as client :
11 r = await client . get ( " https :// api . example . com / data " )
12 return r . json ()
13
14 # Sync client + sync route = also fine ( runs in a thread pool )
15 @app . get ( " / proxy - ok " )
16 def proxy_ok () :
17 r = requests . get ( " https :// api . example . com / data " )
18 return r . json ()
19
20 # CPU - bound work : keep sync
21 @app . post ( " / predict " )
22 def predict ( features : list [ float ]) :
23 return { " prediction ": model . predict ([ features ]) . tolist () }

Deep Dive

The simple rule. If you can write the whole function body using libraries that sup-
port await, use async def. Otherwise, write a plain def. Never mix async def with
synchronous blocking calls  that combination has the worst of both worlds: thread-pool
unavailable, event-loop blocked.

4.10 A Concrete Async Win  Calling Two APIs in Parallel


This is where async shines for AI engineering. Imagine you need to call OpenAI and Anthropic
in parallel and return whichever nishes rst, or both:

Listing 20: Parallel API calls with [Link] 


1 import asyncio , httpx
2 from fastapi import FastAPI
3
4 app = FastAPI ()
5
6 async def call_openai ( prompt : str ) -> str :
7 async with httpx . AsyncClient ( timeout =30) as client :
8 r = await client . post ( " https :// api . openai . com / v1 /... " ,
9 json ={ " prompt " : prompt })
10 return r . json () [ " choices " ][0][ " text " ]
11
12 async def call_anthropic ( prompt : str ) -> str :

20
FastAPI  Module 3 5 SUMMARY  ONE-PAGE MENTAL MODEL

13 async with httpx . AsyncClient ( timeout =30) as client :


14 r = await client . post ( " https :// api . anthropic . com / v1 /... " ,
15 json ={ " prompt " : prompt })
16 return r . json () [ " content " ][0][ " text " ]
17
18 @app . post ( " / compare " )
19 async def compare ( prompt : str ) :
20 # Both calls launch concurrently ; total time = max of the two ,
21 # not the sum . This is impossible in synchronous code without threads .
22 openai_resp , anthropic_resp = await asyncio . gather (
23 call_openai ( prompt ) ,
24 call_anthropic ( prompt ) ,
25 )
26 return { " openai " : openai_resp , " anthropic " : anthropic_resp }

If each call takes 4 seconds, the sync version takes 8 seconds. The async version takes 4. That
doubling of throughput compounds as you add more parallel calls  which is why every modern
LLM-powered backend is async.

5 Summary  One-Page Mental Model


ˆ A route = HTTP method + URL path + path operation function. The decorator (@[Link](...))
is sugar for app.add_api_route(...).

ˆ Return types: dict (auto-JSON), Pydantic model (recommended, validates output), or cus-
tom Response (HTML, streaming, etc.).

ˆ HTTP verbs: GET (read, idempotent), POST (create), PUT (replace), PATCH (partial
update), DELETE (remove, idempotent).

ˆ CRUD pattern: list / get one / create / update / delete  ve endpoints per resource, with
separate ResourceIn and ResourceOut models.

ˆ Three validation layers: type hint → Field() constraints → @field_validator for cross-
eld logic. All errors are collected and returned together.

ˆ HTTPException is how you abort with a custom status code and message.

ˆ Async: async def runs on the event loop  use with async I/O libraries. def runs in a thread
pool  safe for blocking code. Never mix async def with blocking calls.

ˆ Async wins big when calling multiple slow services in parallel  the dening workload of
modern AI backends.

End of Module 3  Building APIs

21

You might also like