Build a Bookshelf REST API with Flask
Build a CRUD REST API for a book collection using Python Flask.
Introduction
30 Second Summary
Every time you search for a product online, check your bank balance, or scroll a social media feed, an API is working behind the scenes to deliver that data to your screen.
In this project, you will build a REST API from scratch using Python and Flask. You'll create endpoints that let clients create, read, update, and delete books in a virtual bookshelf.
What You'll Build
A Bookshelf API server that manages a collection of books through standard HTTP methods, returns proper status codes, and handles errors with structured JSON responses.
By the end of this project, you'll have:
- A Flask application with 5 RESTful endpoints that support full CRUD operations (Create, Read, Update, Delete).
- An in-memory data store seeded with sample book data that your API reads from and writes to.
- Custom error handlers that return consistent JSON error responses with proper HTTP status codes.
- Secret Mission: Add query parameter filtering to search books by author or title.
Are there any prerequisites?
This project assumes you're comfortable with Python fundamentals like functions, dictionaries, decorators, and list comprehensions. No prior experience with Flask or REST APIs is needed.
Set Up Your Environment
Before writing any API logic, you need to confirm that Flask can serve HTTP responses from your environment. A quick starter app verifies that the framework, the dev server, and your editor are all working together.
You'll create a single file that defines a root route and returns a JSON welcome message. Once the server is running, you'll have a foundation to build every endpoint on.
In this step, get ready to:
- Create app.py with a minimal Flask application and a root route.
- Run the Flask development server and confirm it serves a JSON response.
Create your Flask application file
Your Codespace already has Python, a virtual environment, and Flask ready to go. The REST Client extension is installed too. The only thing missing is your application code.
- Create a new file called app.py in your project directory by pasting the following code:
from flask import Flask, request, jsonify
# Create the Flask application instance
app = Flask(__name__)
# Root route - lists available API endpoints
@app.route("/")
def index():
return jsonify({"message": "Welcome to the Bookshelf API", "endpoints": {
"GET /books": "List all books",
"GET /books/<id>": "Get a single book",
"POST /books": "Add a new book",
"PUT /books/<id>": "Update a book",
"DELETE /books/<id>": "Delete a book",
}})
What does this code do?
- Flask(__name__) creates your application instance. Flask uses __name__ to locate templates and static files relative to your module.
- @app.route("/") registers the index() function as the handler for the root URL. Any GET request to / triggers this function.
- jsonify() converts your Python dictionary into a JSON HTTP response with the correct Content-Type header.
- The request import isn't used yet. You'll need it in the next step when reading data from incoming requests.
- Save app.py.
- Start the Flask development server by running this command in the terminal:
flask --app app run --debug
What does debug mode do?
The --debug flag enables two features: auto-reload (the server restarts whenever you save a file) and an interactive debugger (you see detailed error pages instead of a generic 500). You'll keep debug mode on for the entire project.
You should see output showing the server running on http://127.0.0.1:5000. Your Codespace will show a notification that the port is available. Click Open in Browser to visit the URL.
You should see a JSON object with a welcome message and a list of endpoints.
Not seeing the JSON response?
- Make sure your virtual environment is active. Your terminal prompt should show (.venv) at the beginning. If not, run . .venv/bin/activate and try the flask command again.
- If you see "Address already in use", another process is using port 5000. Stop it by pressing Ctrl+C in the terminal, then rerun the command.
- Check that your file is saved and named exactly app.py (not App.py or apps.py).
Still stuck? Help me debug why my Flask server isn't starting.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file, then leave the server running in the terminal.
ⓧ I'd like to double check the full code
from flask import Flask, request, jsonify
# Create the Flask application instance
app = Flask(__name__)
# Root route - lists available API endpoints
@app.route("/")
def index():
return jsonify({"message": "Welcome to the Bookshelf API", "endpoints": {
"GET /books": "List all books",
"GET /books/<id>": "Get a single book",
"POST /books": "Add a new book",
"PUT /books/<id>": "Update a book",
"DELETE /books/<id>": "Delete a book",
}})
Your Flask server is live and responding with JSON. Next up, you'll add real data and build the first read endpoints for your bookshelf.
Build the Data Layer and First Endpoints
Your Flask app is running and responding to requests at the root URL. Right now though, it only returns a static welcome message.
Every API needs data to serve. In this step, you'll create an in-memory data store seeded with sample books, then build your first real endpoints to read from it.
In this step, get ready to:
- Define sample book data in an in-memory Python list.
- Create a GET endpoint that returns all books.
- Create a GET endpoint that returns a single book by ID or a 404 error.
Define the in-memory data store
Instead of connecting to a database, you'll store books in a plain Python list. This keeps the project focused on REST patterns without introducing database setup.
- In app.py, find the line that says app = Flask(__name__) and add the following code below it:
# In-memory data store
books = [
{"id": 1, "title": "The Hobbit", "author": "J.R.R. Tolkien", "year": 1937, "genre": "Fantasy"},
{"id": 2, "title": "1984", "author": "George Orwell", "year": 1949, "genre": "Dystopian"},
{"id": 3, "title": "Dune", "author": "Frank Herbert", "year": 1965, "genre": "Science Fiction"},
]
next_id = 4
What does this code do?
The books list holds three dictionaries, each representing a book with an id, title, author, year, and genre.
The next_id variable tracks the ID to assign to the next book that gets created. Since the seed data uses IDs 1 through 3, the next available ID is 4.
- Save app.py.
- Check the terminal where Flask is running. You should see a "Restarting with stat" message, confirming the server reloaded without errors.
Seeing a syntax error in the terminal?
- Check that each dictionary in the books list ends with a comma, including the last one before the closing ].
- Ensure next_id = 4 is outside the list, not inside the square brackets.
- help me fix a syntax error in my Flask books data store
Create the GET /books endpoint
Your data exists, but there's no way to access it over HTTP yet. This endpoint returns the entire book collection as JSON.
- In app.py, scroll down past the index() function and add the following code below it:
@app.route("/books", methods=["GET"])
def get_books():
return jsonify(books)
What does this code do?
The @app.route decorator maps the URL /books to this function, but only for GET requests. The methods=["GET"] parameter makes this explicit.
The jsonify(books) call converts the Python list into a JSON response with the correct Content-Type header.
- Save app.py.
- Switch to the Ports tab in the VS Code terminal panel.
- Click the globe icon next to port 5000 to open the forwarded URL in your browser.
- Append /books to the URL in the browser address bar and press Enter.
- You should see a JSON array containing all 3 books.
Seeing a 404 or empty page?
- Make sure the URL ends with /books (not /book or /Books). Flask routes are case-sensitive.
- Confirm you saved app.py and the terminal shows the server reloaded.
- help me debug my GET /books endpoint returning 404
Create the GET /books/<id> endpoint
Listing all books is useful, but clients often need to fetch a single specific book. This endpoint uses a URL parameter to look up one book by its ID.
- In app.py, add the following code below the get_books() function:
@app.route("/books/<int:book_id>", methods=["GET"])
def get_book(book_id):
# Search for the book by ID
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Not Found", "message": f"Book with id {book_id} not found"}), 404
return jsonify(book)
What does this code do?
The <int:book_id> in the route tells Flask to capture that part of the URL as an integer and pass it to the function as the book_id parameter.
The next() call searches through the books list using a generator expression. If no book matches the given ID, it returns None instead of raising an error.
When the book isn't found, the function returns a JSON error object with a 404 status code. This is the standard HTTP status code for "Not Found".
- Save app.py.
- In your browser, change the URL to end with /books/1 and press Enter.
- You should see a single JSON object for "The Hobbit".
- Now change the URL to end with /books/999 and press Enter.
- You should see a JSON error response with a 404 status.
Getting a generic HTML error instead of JSON?
- Make sure you typed <int:book_id> in the route decorator, not <book_id> without the int: prefix. Without it, Flask passes a string and the ID comparison fails.
- Check that the if book is None: block returns jsonify(...) with a tuple , 404 at the end.
- help me debug my GET /books/<id> endpoint returning HTML instead of JSON
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py and your server is still running.
ⓧ I'd like to double check the full code
Here's what your complete app.py should look like at this point:
from flask import Flask, request, jsonify
app = Flask(__name__)
# In-memory data store
books = [
{"id": 1, "title": "The Hobbit", "author": "J.R.R. Tolkien", "year": 1937, "genre": "Fantasy"},
{"id": 2, "title": "1984", "author": "George Orwell", "year": 1949, "genre": "Dystopian"},
{"id": 3, "title": "Dune", "author": "Frank Herbert", "year": 1965, "genre": "Science Fiction"},
]
next_id = 4
@app.route("/")
def index():
return jsonify({"message": "Welcome to the Bookshelf API", "endpoints": {
"GET /books": "List all books",
"GET /books/<id>": "Get a single book",
"POST /books": "Add a new book",
"PUT /books/<id>": "Update a book",
"DELETE /books/<id>": "Delete a book",
}})
@app.route("/books", methods=["GET"])
def get_books():
return jsonify(books)
@app.route("/books/<int:book_id>", methods=["GET"])
def get_book(book_id):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Not Found", "message": f"Book with id {book_id} not found"}), 404
return jsonify(book)
Your API can now read data. Next up, you'll add the ability to create and delete books, turning this into a fully interactive service.
Create and Delete Books
Your API can read books, but a real bookshelf needs to grow and shrink. Clients need to add new books and remove old ones.
In this step, you'll build two new endpoints: POST to create books with input validation, and DELETE to remove them. You'll also create a .http test file to exercise every endpoint from inside VS Code.
In this step, get ready to:
- Create a POST endpoint that validates input and returns new books with a 201 status.
- Create a DELETE endpoint that removes a book by ID.
- Build a test file that sends requests to every endpoint.
Create the POST /books endpoint
A POST request tells the server to create a new resource. Your endpoint needs to read the JSON body, check that required fields are present, and return the new book with a 201 status code (meaning "Created").
- In app.py, scroll to the bottom of the file (below the get_book function) and add the following code:
@app.route("/books", methods=["POST"])
def create_book():
global next_id
# Parse the JSON request body
data = request.get_json()
# Reject requests without valid JSON
if not data:
return jsonify({"error": "Bad Request", "message": "Request body must be JSON"}), 400
# Validate that title and author are provided
if "title" not in data or "author" not in data:
return jsonify({"error": "Bad Request", "message": "Fields 'title' and 'author' are required"}), 400
# Build the new book with an auto-incremented ID
new_book = {
"id": next_id,
"title": data["title"],
"author": data["author"],
"year": data.get("year"),
"genre": data.get("genre"),
}
next_id += 1
books.append(new_book)
# Return the created book with 201 Created status
return jsonify(new_book), 201
What does this code do?
This function handles incoming POST requests to /books.
- global next_id lets the function modify the module-level counter so each book gets a unique ID.
- request.get_json() parses the request body as JSON. If the body is missing or malformed, it returns None.
- Two validation checks return a 400 Bad Request status if the data is invalid.
- data.get("year") safely fetches optional fields, defaulting to None if the client didn't provide them.
- Returning a tuple (response, 201) sets the HTTP status code to 201 Created instead of the default 200.
- Save app.py.
- In the terminal where Flask is running, you should see it detect the change and restart the server automatically.
Server didn't restart?
Make sure your server was started with the --debug flag. Without it, Flask won't auto-reload on file changes.
If you stopped the server earlier, restart it by running flask --app app run --debug in the terminal.
help me debug why Flask isn't restarting when I save app.py
Create the DELETE /books endpoint
A DELETE request removes a resource from the server. Your endpoint finds the book by ID, removes it from the list, and confirms the deletion.
- In app.py, add the following code below the create_book function you just wrote:
@app.route("/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
global books
# Find the book to delete
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Not Found", "message": f"Book with id {book_id} not found"}), 404
# Remove the book by rebuilding the list without it
books = [b for b in books if b["id"] != book_id]
return jsonify({"message": f"Book with id {book_id} deleted"})
What does this code do?
This function handles DELETE requests to /books/<int:book_id>.
- global books is needed because the function reassigns the entire list variable (not just mutating it).
- The same next() pattern from the GET endpoint checks whether the book exists before attempting deletion.
- A list comprehension rebuilds books without the deleted entry. This is a clean way to filter in Python.
- Save app.py.
- Confirm Flask restarts again in the terminal.
Seeing a NameError?
If you see an error about books not being defined, make sure global books is the first line inside the delete_book function.
help me fix a NameError in my delete_book function
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py and the server has restarted.
ⓧ I'd like to double check the full code
from flask import Flask, request, jsonify
app = Flask(__name__)
# In-memory data store
books = [
{"id": 1, "title": "The Hobbit", "author": "J.R.R. Tolkien", "year": 1937, "genre": "Fantasy"},
{"id": 2, "title": "1984", "author": "George Orwell", "year": 1949, "genre": "Dystopian"},
{"id": 3, "title": "Dune", "author": "Frank Herbert", "year": 1965, "genre": "Science Fiction"},
]
next_id = 4
@app.route("/")
def index():
return jsonify({"message": "Welcome to the Bookshelf API", "endpoints": {
"GET /books": "List all books",
"GET /books/<id>": "Get a single book",
"POST /books": "Add a new book",
"PUT /books/<id>": "Update a book",
"DELETE /books/<id>": "Delete a book",
}})
@app.route("/books", methods=["GET"])
def get_books():
return jsonify(books)
@app.route("/books/<int:book_id>", methods=["GET"])
def get_book(book_id):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Not Found", "message": f"Book with id {book_id} not found"}), 404
return jsonify(book)
@app.route("/books", methods=["POST"])
def create_book():
global next_id
data = request.get_json()
if not data:
return jsonify({"error": "Bad Request", "message": "Request body must be JSON"}), 400
if "title" not in data or "author" not in data:
return jsonify({"error": "Bad Request", "message": "Fields 'title' and 'author' are required"}), 400
new_book = {
"id": next_id,
"title": data["title"],
"author": data["author"],
"year": data.get("year"),
"genre": data.get("genre"),
}
next_id += 1
books.append(new_book)
return jsonify(new_book), 201
@app.route("/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
global books
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Not Found", "message": f"Book with id {book_id} not found"}), 404
books = [b for b in books if b["id"] != book_id]
return jsonify({"message": f"Book with id {book_id} deleted"})
Create the .http test file
The REST Client extension lets you write and send HTTP requests directly from a file in VS Code. Each request is separated by a ### delimiter, and you send them by clicking the Send Request link that appears above each one.
- Create a new file called test_api.http in the same folder as app.py.
- Paste the following content into test_api.http:
### Welcome
GET http://127.0.0.1:5000/
### Get all books
GET http://127.0.0.1:5000/books
### Get a single book
GET http://127.0.0.1:5000/books/1
### Get non-existent book (expect 404)
GET http://127.0.0.1:5000/books/999
### Create a new book (valid)
POST http://127.0.0.1:5000/books
Content-Type: application/json
{
"title": "Neuromancer",
"author": "William Gibson",
"year": 1984,
"genre": "Cyberpunk"
}
### Create a book (missing required fields - expect 400)
POST http://127.0.0.1:5000/books
Content-Type: application/json
{
"year": 2000
}
### Delete a book
DELETE http://127.0.0.1:5000/books/3
### Verify deletion (expect 404)
GET http://127.0.0.1:5000/books/3
What does this file do?
Each ### line separates individual requests. The REST Client extension reads this file and adds a clickable Send Request link above each one.
- GET requests test reading data (all books, one book, missing book).
- POST requests test creating data. The Content-Type: application/json header tells Flask the body is JSON. The blank line between the header and the body is required.
- The DELETE request removes a book, and the follow-up GET confirms it's gone.
- Save test_api.http.
- Click the Send Request link above ### Create a new book (valid).
- A response panel opens on the right showing the new book JSON with a 201 Created status at the top.
What should I see?
The response panel shows HTTP/1.1 201 CREATED at the top, followed by a JSON body containing the new book with an id of 4.
Try the "missing required fields" POST next. You should see a 400 BAD REQUEST status with an error message about title and author being required.
- Click Send Request above ### Delete a book.
- Then click Send Request above ### Verify deletion (expect 404) to confirm the book is gone.
Seeing "Connection refused"?
This means the Flask server isn't running. Switch to the terminal and run flask --app app run --debug to start it again.
If you see "Send Request" links but no response panel appears, make sure the REST Client extension is installed and enabled in your Codespace.
help me troubleshoot why my REST Client requests aren't getting a response from Flask
Your API now handles creating and deleting books with proper status codes and validation. Next up, you'll add the PUT endpoint for updating books and build custom error handlers to keep every error response consistent.
Update Books and Add Error Handling
Your API can already create, read, and delete books. The last CRUD operation is Update, which lets clients modify an existing book without deleting and recreating it.
You'll also add centralized error handlers so that every error your API returns follows the same JSON structure. Consistent error formats make life easier for anyone consuming your API.
In this step, get ready to:
- Build a PUT endpoint that updates a book's fields.
- Add custom error handlers for 404 and 400 responses.
- Extend your test file with PUT requests and verify consistent error JSON.
Create the PUT endpoint
PUT is the HTTP method for replacing or updating an existing resource. Your endpoint will find the book by ID, validate the incoming JSON, then update only the fields the client sends.
- In app.py, find the delete_book function near the bottom of the file.
- Add the following update_book function directly above delete_book:
@app.route("/books/<int:book_id>", methods=["PUT"])
def update_book(book_id):
# Find the book by ID
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Not Found", "message": f"Book with id {book_id} not found"}), 404
# Validate incoming JSON
data = request.get_json()
if not data:
return jsonify({"error": "Bad Request", "message": "Request body must be JSON"}), 400
# Update fields, falling back to existing values if not provided
book["title"] = data.get("title", book["title"])
book["author"] = data.get("author", book["author"])
book["year"] = data.get("year", book["year"])
book["genre"] = data.get("genre", book["genre"])
return jsonify(book)
What does this code do?
The function looks up the book the same way get_book does. If the book doesn't exist, the endpoint returns a 404 immediately.
The data.get("title", book["title"]) pattern is the key trick. It uses the new value from the request if provided, but falls back to the book's current value if that field wasn't sent. This means clients can update just one field without overwriting the rest.
- Save app.py.
Seeing a syntax error on save?
Make sure the new function is at the same indentation level as the other route functions. Each @app.route decorator should start at column 0.
Help me debug this syntax error
Add custom error handlers
Right now, your route functions return JSON errors manually with inline jsonify calls. Flask also has built-in error pages (like the default HTML 404 page) that fire when something unexpected happens. Custom error handlers ensure those built-in errors also return JSON instead of HTML.
- In app.py, find this line near the top of the file:
next_id = 4
- Add the following error handlers below next_id = 4. Also add a # Routes comment above your existing @app.route("/") line to keep the file organized:
# Error handlers
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "Not Found", "message": str(e)}), 404
@app.errorhandler(400)
def bad_request(e):
return jsonify({"error": "Bad Request", "message": str(e)}), 400
# Routes
What do error handlers do?
The @app.errorhandler(404) decorator registers a function that Flask calls whenever a 404 error is raised anywhere in your app. Without this, Flask would return an HTML error page that breaks API clients expecting JSON.
The str(e) converts the exception into a human-readable description. This gives callers a useful error message alongside the status code.
- Save app.py. The dev server auto-reloads because you started Flask with --debug.
Server didn't reload?
Check your terminal for a traceback. A missing colon after def not_found(e) or incorrect indentation will prevent the reload. Fix the error and save again.
Help me debug my error handlers
Test the PUT endpoint and verify error responses
Your API now has all five endpoints. Time to add PUT test requests to your test file and verify that every error response uses the same JSON structure.
- Switch back to test_api.http in the editor.
- Find the ### Delete a book line near the bottom.
- Add the following two test requests directly above ### Delete a book:
### Update a book
PUT http://127.0.0.1:5000/books/1
Content-Type: application/json
{
"title": "The Hobbit: Or There and Back Again",
"year": 1937
}
### Update non-existent book (expect 404)
PUT http://127.0.0.1:5000/books/999
Content-Type: application/json
{
"title": "Ghost Book"
}
What does this test?
The first PUT sends a partial update to book 1. Only title and year are in the body, so author and genre should remain unchanged in the response.
The second PUT targets a book ID that doesn't exist. You should get a 404 JSON error with the same {"error": ..., "message": ...} structure.
- Save test_api.http.
- Click Send Request above the ### Update a book line. You should see a 200 response with the updated title and unchanged author/genre fields.
- Click Send Request above the ### Update non-existent book (expect 404) line. You should see a 404 JSON error response.
Getting HTML instead of JSON for errors?
Make sure your error handlers are placed above the route definitions in app.py. Also confirm the file was saved and the dev server reloaded without errors.
Help me debug my JSON error responses
✔️ Awesome, I've got everything!
Great. Double check you've saved both files.
ⓧ I'd like to double check the full code
Here is the complete app.py at this point:
from flask import Flask, request, jsonify
app = Flask(__name__)
# In-memory data store
books = [
{"id": 1, "title": "The Hobbit", "author": "J.R.R. Tolkien", "year": 1937, "genre": "Fantasy"},
{"id": 2, "title": "1984", "author": "George Orwell", "year": 1949, "genre": "Dystopian"},
{"id": 3, "title": "Dune", "author": "Frank Herbert", "year": 1965, "genre": "Science Fiction"},
]
next_id = 4
# Error handlers
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "Not Found", "message": str(e)}), 404
@app.errorhandler(400)
def bad_request(e):
return jsonify({"error": "Bad Request", "message": str(e)}), 400
# Routes
@app.route("/")
def index():
return jsonify({"message": "Welcome to the Bookshelf API", "endpoints": {
"GET /books": "List all books",
"GET /books/<id>": "Get a single book",
"POST /books": "Add a new book",
"PUT /books/<id>": "Update a book",
"DELETE /books/<id>": "Delete a book",
}})
@app.route("/books", methods=["GET"])
def get_books():
return jsonify(books)
@app.route("/books/<int:book_id>", methods=["GET"])
def get_book(book_id):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Not Found", "message": f"Book with id {book_id} not found"}), 404
return jsonify(book)
@app.route("/books", methods=["POST"])
def create_book():
global next_id
data = request.get_json()
if not data:
return jsonify({"error": "Bad Request", "message": "Request body must be JSON"}), 400
if "title" not in data or "author" not in data:
return jsonify({"error": "Bad Request", "message": "Fields 'title' and 'author' are required"}), 400
new_book = {
"id": next_id,
"title": data["title"],
"author": data["author"],
"year": data.get("year"),
"genre": data.get("genre"),
}
next_id += 1
books.append(new_book)
return jsonify(new_book), 201
@app.route("/books/<int:book_id>", methods=["PUT"])
def update_book(book_id):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Not Found", "message": f"Book with id {book_id} not found"}), 404
data = request.get_json()
if not data:
return jsonify({"error": "Bad Request", "message": "Request body must be JSON"}), 400
book["title"] = data.get("title", book["title"])
book["author"] = data.get("author", book["author"])
book["year"] = data.get("year", book["year"])
book["genre"] = data.get("genre", book["genre"])
return jsonify(book)
@app.route("/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
global books
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Not Found", "message": f"Book with id {book_id} not found"}), 404
books = [b for b in books if b["id"] != book_id]
return jsonify({"message": f"Book with id {book_id} deleted"})
And here is the complete test_api.http:
### Welcome
GET http://127.0.0.1:5000/
### Get all books
GET http://127.0.0.1:5000/books
### Get a single book
GET http://127.0.0.1:5000/books/1
### Get non-existent book (expect 404)
GET http://127.0.0.1:5000/books/999
### Create a new book (valid)
POST http://127.0.0.1:5000/books
Content-Type: application/json
{
"title": "Neuromancer",
"author": "William Gibson",
"year": 1984,
"genre": "Cyberpunk"
}
### Create a book (missing required fields - expect 400)
POST http://127.0.0.1:5000/books
Content-Type: application/json
{
"year": 2000
}
### Update a book
PUT http://127.0.0.1:5000/books/1
Content-Type: application/json
{
"title": "The Hobbit: Or There and Back Again",
"year": 1937
}
### Update non-existent book (expect 404)
PUT http://127.0.0.1:5000/books/999
Content-Type: application/json
{
"title": "Ghost Book"
}
### Delete a book
DELETE http://127.0.0.1:5000/books/3
### Verify deletion (expect 404)
GET http://127.0.0.1:5000/books/3
Your API now supports all five CRUD operations with consistent JSON error handling. Next up, you'll take on the Secret Mission and add search filtering to find specific books.
Secret mission
Add Filtering and Search
Your API returns all books every time. Real APIs let users narrow down results with query parameters. Can you add filtering to your GET /books endpoint so users can search by author, title, or genre with partial, case-insensitive matching?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs inside a GitHub Codespace, so a running Codespace consumes your free monthly compute hours.
Cost warning
Running Codespaces incur CPU charges against your free monthly allowance (120 core-hours for GitHub Free accounts). A stopped Codespace only incurs storage costs. If you are not actively working on this project, stop or delete the Codespace to avoid burning through your free hours.
Resources you used:
- GitHub Codespace (cloud compute and storage).
- Flask dev server (running process inside the Codespace).
- Project files: app.py and test_api.http (stored inside the Codespace).
Keep everything running
No action needed. Choose this if you are still actively building or plan to continue immediately.
- Your Codespace stays running and your Flask server keeps serving requests.
- The Codespace will auto-stop after 30 minutes of inactivity, preserving all saved changes.
- You can reopen it anytime from github.com/codespaces.
Pause - I'll come back to this later
Stop the Codespace to free up compute hours while keeping your files intact.
- In the Codespace terminal, press Ctrl+C to stop the Flask dev server.
- Navigate to github.com/codespaces in your browser.
- Click the ellipsis (...) to the right of your Codespace.
- Click Stop codespace.
- Your files and virtual environment remain saved. Reopen the Codespace later to resume where you left off.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your code and environment.
- In the Codespace terminal, press Ctrl+C to stop the Flask dev server.
- Navigate to github.com/codespaces in your browser.
- Click the ellipsis (...) to the right of your Codespace.
- Click Delete.
- Confirm the deletion when prompted. This removes the Codespace, all project files, the virtual environment, and installed extensions permanently.
Nice Work!
Nice Work!
Nice work! You just built a fully functional REST API from scratch using Flask. Your Bookshelf API can create, read, update, and delete books, validate input, and return clean JSON error responses.
You've learned how to:
- Build a REST API server with full CRUD operations (Create, Read, Update, Delete) using proper HTTP methods and Flask.
- Return correct HTTP status codes (200, 201, 400, 404) with input validation and consistent JSON error handling.
- Test every endpoint using .http files with the REST Client extension in VS Code.
- Secret Mission: Added query parameter filtering with case-insensitive partial matching to search books by author, title, or genre.
Ready to quiz yourself?