Backend Basics Checkpoint
A review of this module. Skip it if you feel confident; the next module does not need it.
Fix the mistakes
The slips that cost marks in this module. Spot each one, then check the fix.
Test what you know about HTTP, FastAPI routes, Pydantic, the database, login and tests. Most problems use only the standard library, so they run as one file. They check the ideas your two resume projects depend on.
Requests, routes and status codes
The method says what to do and the path names the thing. The status code tells how it went: 2xx worked, 4xx the caller made a mistake, 5xx the server failed.
@app.get("/products/{product_id}")def get_product(product_id: int, db: Session = Depends(get_db)): product = db.get(Product, product_id) if product is None: raise HTTPException(status_code=404, detail="Product not found") return productExample 1 · Read a request and name the result
from urllib.parse import parse_qs, urlsplit
FAMILIES = {2: "success", 4: "client error", 5: "server error"}
def describe(request_line, status): method, target, _ = request_line.split() parts = urlsplit(target) limit = parse_qs(parts.query).get("limit", ["10"])[0] return f"{method} {parts.path} limit={limit} -> {status} {FAMILIES[status // 100]}"
print(describe("GET /products?limit=5 HTTP/1.1", 200))print(describe("GET /products/99 HTTP/1.1", 404))print(describe("POST /products HTTP/1.1", 500))GET /products limit=5 -> 200 success GET /products/99 limit=10 -> 404 client error POST /products limit=10 -> 500 server error
What this code does
request_line.split()gives the method, the target and the HTTP version.parse_qs(...).get("limit", ["10"])reads the query and falls back to a default, just as a route default does.status // 100picks the family of the status code.
A path parameter picks one record. A missing record is 404. A request body that breaks a Pydantic rule never reaches this function; FastAPI answers 422 first.
Models, sessions and pages
A model class is a table and a session saves changes on commit(). Every list query takes skip and limit, so one response can never carry a whole table.
with SessionLocal() as db: query = select(Ticket).order_by(Ticket.id).offset(skip).limit(limit) tickets = db.scalars(query).all()Example 2 · Skip and limit as a slice
def page(items, skip=0, limit=50): return items[skip:skip + limit]
orders = list(range(1, 8))print(page(orders))print(page(orders, skip=0, limit=3))print(page(orders, skip=3, limit=3))print(page(orders, skip=6, limit=3))print(page(orders, skip=9, limit=3))[1, 2, 3, 4, 5, 6, 7] [1, 2, 3] [4, 5, 6] [7] []
What this code does
- A slice that runs past the end is simply shorter, so the last page can hold fewer items.
- A page that starts past the end is an empty list, not an error.
- The database does the same with
offset(skip).limit(limit).
Example 3 · The same page from SQLite
from sqlalchemy import String, create_engine, selectfrom sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
class Base(DeclarativeBase): pass
class Ticket(Base): __tablename__ = "tickets" id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column(String(100))
engine = create_engine("sqlite:///:memory:")Base.metadata.create_all(engine)
with Session(engine) as session: session.add_all([Ticket(title=f"Ticket {n}") for n in range(1, 8)]) session.commit() query = select(Ticket.title).order_by(Ticket.id).offset(3).limit(3) print(session.scalars(query).all())['Ticket 4', 'Ticket 5', 'Ticket 6']
What this code does
order_bykeeps the pages stable. Without an order the database may return rows in any order.offset(3).limit(3)skips three rows and takes the next three, so you get tickets 4 to 6.
The session opens in get_db and closes when the request ends. In tests, app.dependency_overrides[get_db] swaps in an empty in-memory database.
Login, roles and tests
Store a salted hash, never the password. A login returns a signed token; each protected route decodes it. A missing or bad token is 401, and a wrong role is 403.
def test_admin_route_needs_admin(client, learner_headers): response = client.get("/admin/report", headers=learner_headers) assert response.status_code == 403
def test_me_needs_a_token(client): assert client.get("/me").status_code == 401Test the failures as well as success. The README then gives the commands to install, run and test the project.
Module quiz
Questions from the whole module. Your first answer is your score.
Problems
Graded easy to hard, using everything from this module.
Try each one yourself first. Use the hint if you get stuck. Mark it as done when it works.
Exercise 1· Easy
Print the status code that fits each situation, one per line: a record was created, the record does not exist, the body is invalid, the caller is not logged in, the caller has the wrong role. Use the codes 201, 404, 422, 401 and 403 in that order of situations.
created 201 missing record 404 invalid body 422 not logged in 401 wrong role 403
Exercise 2· Easy
Write page(items, skip, limit) that returns the slice starting at skip with at most limit items. For the list 1 to 10, print page(items, 3, 4) and page(items, 8, 4).
[4, 5, 6, 7] [9, 10]
Exercise 3· Easy
Split the request target /products/7?limit=5 into the path, the product id as an integer and the limit as an integer. Print them as path=... id=... limit=....
path=/products/7 id=7 limit=5
Exercise 4· Medium
Write check(password, salt, stored) with hashlib.pbkdf2_hmac("sha256", ..., 100_000) that compares with hmac.compare_digest. Store the hash of S3cret-pass with the salt bytes.fromhex("0a0b0c0d0e0f1011"), then print the result of check for S3cret-pass and for s3cret-pass.
True False
Exercise 5· Medium
A token carries an exp time in seconds. Write is_expired(exp, now) that is true when now is at or after exp. With now = 1000, print the result for exp values 1500, 1000 and 400.
False True True
Exercise 6· Medium
Write match(pattern, path) where the pattern may hold one {name} part, such as /products/{id}. Return a dictionary of the matched value, or None when the path does not fit. Print match("/products/{id}", "/products/7"), match("/products/{id}", "/orders/7") and match("/health", "/health").
{'id': '7'}
None
{}Exercise 7· Hard
Write check_product(data) that returns a list of problems for a dictionary: name must be text of 2 to 60 characters and price must be a number above 0. A missing key is a problem too. Print the list for {"name": "Pen", "price": 25}, for {"name": "P", "price": -1} and for {}. Use the messages name must be 2 to 60 characters and price must be above 0.
[] ['name must be 2 to 60 characters', 'price must be above 0'] ['name must be 2 to 60 characters', 'price must be above 0']
Exercise 8· Hard
Tokens map to users: t1 is asha (role learner) and t2 is ravi (role admin). A route needs the role admin. Write answer(token, needed) that returns 401 for an unknown or missing token, 403 for a wrong role and 200 otherwise. Print the result for None, "bad", "t1" and "t2".
401 401 403 200
Interview questions
Questions interviewers ask on this topic.
What's next
You finished the preview of Backend Basics for Projects
Python Programming has 21 sections, 110 modules and 609 lessons. An account opens every lesson and saves your progress.
Finish up
Mark complete & continue
Next: Online Store API: FastAPI, SQLAlchemy and JWT