Checkpoint: Build a REST API
A REST API is a set of HTTP endpoints that expose your data as JSON, letting clients create, read, and page through resources using standard methods and status codes.
Learn Checkpoint: Build a REST API in our free Flask course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Flask course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
This checkpoint combines everything from the API track — models, queries, pagination, validation, and serialization — into one runnable REST endpoint you build and test yourself.
You've covered the full toolkit for building production APIs. Here's the recap:
You'll build a small JSON REST API for a task list, combining four skills into one file:
Everything runs in one process with an in-memory database and the test client. The starter below is fully runnable — read it, run it, and watch all four pieces work together.
Output: page 2 lists Task 6 … Task 10 ; the POST returns 201 with the new task; the empty-title POST returns 400 with a validation error. That's a real REST API.
⏱ Timed Quiz
Test yourself — click each question to reveal the answer.
schema.load(data) validates and deserializes input. On bad data it raises a ValidationError , whose .messages is a dict of per-field errors — return it with a 400 .
offset = (page - 1) * per_page . You pass it to .offset(...) alongside .limit(per_page) so the database returns only that page's rows.
201 Created . Return it as the second element of the tuple, e.g. return schema.dump(obj), 201 . A failed validation returns 400 instead.
The id is assigned by the database, not the client. dump_only=True means it appears in output ( dump ) but is ignored on input ( load ), so a client cannot try to set it.
Use a schema created with many=True — e.g. TaskSchema(many=True).dump(rows) — or pass many=True to the call. It returns a list of dicts.
404 Not Found , typically with a small JSON error body like {" "} . Use session.get(Task, id) and check for None .
You forgot to catch ValidationError around schema.load() . Wrap it and return err.messages with a 400 .
The client must send Content-Type: application/json (the test client's json= argument does this automatically). Otherwise the body isn't parsed.
Checkpoint complete — you can ship a real REST API!
You combined a SQLAlchemy model, a Marshmallow schema, pagination, and validation into working JSON endpoints with correct status codes — and tested them in-process.
🚀 Up next: Capstone — A Full CRUD App — bring login, forms, the database, and your API together into one complete application.
Practice quiz
Which Marshmallow method validates and deserializes incoming JSON, and what does it raise on bad data?
- schema.dump(), raising TypeError
- schema.parse(), raising KeyError
- schema.load(), raising ValidationError
- schema.validate(), raising HTTPError
Answer: schema.load(), raising ValidationError. schema.load(data) validates and deserializes input; on bad data it raises ValidationError whose .messages is a dict of per-field errors.
How do you compute the database offset for a given page when paginating?
- (page - 1) * per_page
- page * per_page
- (page + 1) * per_page
- per_page / page
Answer: (page - 1) * per_page. offset = (page - 1) * per_page, passed to .offset() alongside .limit(per_page) so the database returns only that page's rows.
What status code should a successful POST that creates a new resource return?
- 200 OK
- 204 No Content
- 302 Found
- 201 Created
Answer: 201 Created. 201 Created signals a new resource was made; return it as the second tuple element, e.g. return schema.dump(obj), 201.
Why mark the id field as dump_only=True in a Marshmallow schema?
- So it is required on input
- So it appears in output but is ignored on input
- So it is hashed before storage
- So it is excluded from output
Answer: So it appears in output but is ignored on input. The id is assigned by the database, not the client; dump_only=True includes it in dump output but ignores it on load input.
How do you serialize a list of model rows instead of a single object?
- Use a schema created with many=True
- Call schema.dump_many(rows)
- Loop and call dump() per row only
- Pass a list to fields.List()
Answer: Use a schema created with many=True. A schema with many=True (e.g. TaskSchema(many=True).dump(rows)) returns a list of dicts for a collection of rows.
What should GET /api/tasks/<id> return when the requested id does not exist?
- 200 with an empty body
- 400 Bad Request
- 404 Not Found
- 500 Internal Server Error
Answer: 404 Not Found. Use session.get(Task, id); if it returns None, respond 404 Not Found, typically with a small JSON error body.
Which SQLAlchemy methods together fetch just one page's rows from the database?
- .filter() and .group_by()
- .limit() and .offset()
- .join() and .having()
- .slice() and .count()
Answer: .limit() and .offset(). .limit(per_page) caps the number of rows and .offset(...) skips earlier pages, so only the requested slice is fetched.
When schema.load() raises a ValidationError, what status code should the endpoint return?
- 201 Created
- 401 Unauthorized
- 404 Not Found
- 400 Bad Request
Answer: 400 Bad Request. Invalid client input is a 400 Bad Request; return err.messages so the client sees which fields failed.
Why does the checkpoint use create_engine('sqlite://') and app.test_client() together?
- To require a running PostgreSQL server
- To run the whole API in one process with no external setup
- To deploy the API to production
- To enable real network sockets
Answer: To run the whole API in one process with no external setup. An in-memory SQLite database plus Flask's in-process test client let the entire API run in a single process with no external server.
What status code does a successful DELETE that removes a resource and returns no body typically use?
- 200 OK with the deleted object
- 201 Created
- 204 No Content
- 404 Not Found
Answer: 204 No Content. After deleting a resource with no body to return, 204 No Content is the conventional response (404 if the id was missing).