Project — APIs & Model Serving

Project Build this yourself, to the spec and rubric below — the module's standard. You build it on your own dataset, alongside the lessons.

Serve the Adult / Census Income model behind a schema-validated FastAPI /predict endpoint that survives real traffic, plus a batch path that scores many records in one call. The service loads the model once at startup, validates every request against a pydantic schema that excludes any leakage feature, returns a typed response, survives the model being unavailable with a fast error instead of a hang, and documents the latency budget you actually measured.

Dataset Adult / Census Income — the same dataset and repo you carry through every module’s project (the lessons serve the Lending Club and KKBox models; here you stand up a real serving layer yourself)
Start from Fork dutchengineer-org/phase2-starter — ships train.py and the frozen package (run python train.py to produce model/model.joblib, the artifact you serve), so you are never blocked on your own training code. Already forked it in M5? Keep building in that fork — one Phase 2 repo carries M5 through M9
Aim for The package matches the ml-pipeline-starter reference — its TrainedModel.load is exactly what your /predict calls; you load that artifact, you do not retrain it or change the package. No version bump: a caller consuming an API is exactly who the version is a promise to — you depend on it, you do not author it, so bumping it is not yours to do here
Done when It passes every line of the rubric below, then you push to GitHub
Project Build this yourself, to the spec and rubric below — the module's standard. You build it on your own dataset, alongside the lessons.

Serve the Adult / Census Income model behind a schema-validated FastAPI /predict endpoint that survives real traffic, plus a batch path that scores many records in one call. The service loads the model once at startup, validates every request against a pydantic schema that excludes any leakage feature, returns a typed response, survives the model being unavailable with a fast error instead of a hang, and documents the latency budget you actually measured.

Dataset Adult / Census Income — the same dataset and repo you carry through every module’s project (the lessons serve the Lending Club and KKBox models; here you stand up a real serving layer yourself)
Start from Fork dutchengineer-org/phase2-starter — ships train.py and the frozen package (run python train.py to produce model/model.joblib, the artifact you serve), so you are never blocked on your own training code. Already forked it in M5? Keep building in that fork — one Phase 2 repo carries M5 through M9
Aim for The package matches the ml-pipeline-starter reference — its TrainedModel.load is exactly what your /predict calls; you load that artifact, you do not retrain it or change the package. No version bump: a caller consuming an API is exactly who the version is a promise to — you depend on it, you do not author it, so bumping it is not yours to do here
Done when It passes every line of the rubric below, then you push to GitHub

Start here

  1. Continue in your Phase 2 forkdutchengineer-org/phase2-starter (or keep building in the one you forked in M5), then uvicorn census_pipeline.serve:app --reload (the starter scaffolds the app at src/census_pipeline/serve.py)
  2. Work the tasks below in order — each maps to a lesson you just finished, run against your own machine with curl
  3. Check yourself against the rubric, then push and submit

By the end, another engineer should be able to clone your repo, run uvicorn, curl your /predict endpoint with one record or a batch, get a clean prediction, and, when they send garbage or kill the model, get a clear error instead of a wedged service.

The tasks

Do these in order; each maps to a lesson you just finished. Run everything against your own machine: uvicorn census_pipeline.serve:app --reload (the starter scaffolds the app at src/census_pipeline/serve.py), then curl the routes.

1. Get a working /predict endpoint running (from Lesson 1)

  • Build a FastAPI app with a /predict route that takes a record, runs the model from the Phase 2 starter, and returns a prediction, plus a /health route.
  • uvicorn serves it locally and a curl -X POST against /predict with one valid Adult record returns a prediction end to end on the happy path.

2. Make model loading survive real traffic (from Lesson 2)

  • Load the model once at startup (module level / lifespan), not inside the request handler, so the disk-read-and-deserialize cost is paid once, not per call.
  • The handler reuses the already-loaded model object; nothing in the per-request path re-reads the artifact from disk.

3. Make the endpoint reject bad input (from Lesson 3)

  • Add a pydantic request schema with correct types so a malformed field (e.g. a string where a number is expected) is rejected at the boundary with a clear 422, not a cryptic 500 from inside the model.
  • The request schema excludes every leakage feature: any field that would only be known after the income outcome it is predicting must not be a field on the schema, so no caller can ever send one. Audit every column, not one name.

4. Add a batch path and measure what it costs (from Lesson 4)

  • Add a batch route that scores a list of records in one request and returns predictions in the same order and length as the input.
  • Add the serving-contract shape test (the M2 contract, at the serving layer): assert that the single-row path and the batch path produce aligned output for the same records.
  • Measure end-to-end latency at the boundary (not just model.predict) for single-row and batch, and record the numbers.

5. Survive real traffic: tail latency, degradation, scale (from Lesson 5)

  • Make the service survive the model being unavailable: a hard per-request timeout and a health check that reflects the model’s real state, so a dead or hanging model returns a fast, typed 503 instead of holding the connection open.
  • Document a latency budget in p50/p99 terms you actually measured under load, and note where the single box stops scaling (the saturated resource).
Hints
  • The fastest way to catch a per-request load: hit /predict once, then fire a burst of concurrent calls. If the first is fast and the burst cliffs, the joblib.load() is on the per-request path: hoist it to startup.
  • For the leakage audit, write down the prediction moment first (“the instant we score a person’s record”), then for each column ask “is this knowable at that instant?” Anything that is only knowable after the outcome is a leakage field: leave it off the schema and prove it with an assertion over your schema’s model_fields.
  • Measure latency at the boundary with the client, not inside the handler: time the curl/ client round trip so the number includes validation, JSON encode/decode, and framework overhead, which is most of a small model’s response time.

Rubric — your project is done when

This is the standard the module holds you to (each bar maps to the lesson that taught it):

  • /predict runs and serves the modeluvicorn serves it; a curl POST with a valid record returns a prediction end to end. (Lesson 1)
  • Model loaded once at startup — the model is loaded at startup and reused; nothing re-reads the artifact on the per-request path. (Lesson 2)
  • Rejects malformed input with a clear error — a bad field is rejected at the boundary with a clear 422/400, not an opaque 500 from inside the model. (Lesson 3)
  • Request schema excludes every leakage feature — no post-outcome field is on the request schema, proven by a test over the schema’s fields. (Lesson 3)
  • Batch path preserves order, aligned with single-row — the batch route returns predictions in input order and length, and the serving-contract shape test proves single-row and batch produce aligned output. (Lesson 4)
  • Survives the model-unavailable case — a dead or hanging model yields a fast, typed 503, never an open-ended hang. (Lesson 5)
  • Documents its latency budget — a measured p50/p99 budget for single-row and batch, with the scaling ceiling noted. (Lessons 4, 5)

Run the self-audit against your own running service before you submit: for each rubric line, exercise it with curl (send garbage, send a leakage field, kill the model, fire a burst), and if one fails, name which lesson’s failure mode you reintroduced.

Submit

Use the branch workflow from the M1 git lesson, not commits straight to main. Branch off main (git checkout -b m6-prediction-api), build this module’s piece there, and open a pull request to merge it back once it meets the rubric. main stays the last-good version of the product you carry forward, so a half-finished module never breaks what later modules build on.

When your repo meets every rubric line, merge your branch to main, push it to GitHub, and submit the repository URL here. (Submission coming soon.)

Coming soon

This lesson is not published yet. Join the waitlist to hear when it ships.

Coming soon