Book Collection App with Barcode Scanning

Build a Flask web app that scans book barcodes and saves metadata to SQLite.

Introduction

30 Second Summary

You have a shelf full of books but no easy way to keep track of what you own. Every time you are at a bookstore, you wonder whether you already have that copy at home.

In this project, you will build a local web app that lets you scan a book's barcode and instantly catalog it with its title, author, and cover image.

What You'll Build

You scan a book with your phone camera or USB barcode scanner, and within seconds the app fetches the book's full details and saves it to your personal database. Open the app from any device on your WiFi and your entire collection is right there.

By the end of this project, you'll have:

  • A working Flask web app that scans barcodes with your phone camera or USB scanner and auto-populates book details from the Open Library API in under 2 seconds.
  • A SQLite database storing your full collection with cover images, browsable from any device on your local network.
  • An HTTPS-secured app accessible from your phone, tablet, or any computer on your WiFi for camera-based scanning.
  • Secret Mission: Add a live search bar that filters your collection as you type.

Are there any prerequisites?

None. This guide covers everything from installing Python packages to generating SSL certificates. You just need a computer running Ubuntu with VS Code installed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Project

To build your book scanner, you need a clean environment where Python packages stay isolated from the rest of your system.

A virtual environment gives you that isolation. In this step, you will create one, install the two Python packages your app needs, and set up the file structure for a Flask web application.

In this step, get ready to:
  • Create a project directory with a Python virtual environment and install Flask + requests.
  • Create the project file structure for your Flask app.
  • Download the html5-qrcode JavaScript library for local serving.
Create the project directory and virtual environment

Your project needs its own folder and an isolated Python environment so Flask and requests do not interfere with any other Python work on your machine.

  • Open a terminal by pressing Ctrl+Alt+T.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder, move into it, and set up a virtual environment by running these commands:
mkdir book-scanner
cd book-scanner
python3 -m venv venv

What does this do?

`mkdir book-scanner` creates a new folder on your Desktop to hold everything for this project.

`python3 -m venv venv` creates a virtual environment inside a subfolder called venv. This gives you a private copy of Python and pip that won't affect anything else on your system.

  • Activate the virtual environment by running:
source venv/bin/activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you are inside the book-scanner folder. Run pwd to check your current directory. It should end with book-scanner. If not, run cd ~/Desktop/book-scanner and try the source command again.

Help me activate my Python virtual environment.

  • Install Flask and requests by running:
pip install Flask==3.1.3 requests==2.34.2

You should see output ending with a line confirming both packages were successfully installed.

What are these packages?

Flask is a lightweight web framework that lets you serve web pages and build APIs with Python. requests is a library for making HTTP calls to external services. You will use it to fetch book data from Open Library.

  • Verify Flask installed correctly by running:
python3 -m flask --version

Before you run this, what version number do you expect to see?

You should see Flask 3.1.3 printed in the output. This confirms Flask is installed and your virtual environment is working.

Seeing a ModuleNotFoundError?

This usually means the virtual environment is not active. Check that (venv) appears in your prompt. If it does not, run source venv/bin/activate again, then re-run the pip install command.

Help me fix my Flask installation.

Create the project file structure

Flask expects a specific folder layout. You need an app.py file for your server code, a templates/ folder for HTML pages, and a static/ folder for JavaScript and CSS files.

  • Create the file and directories by running these commands:
touch app.py
mkdir templates static
  • Confirm everything was created by running:
ls

You should see app.py, static, templates, and venv listed in the output.

Download the html5-qrcode library

Your app will use a JavaScript library called html5-qrcode to scan barcodes through a phone camera. By downloading it locally instead of loading it from the internet each time, your app works even when your devices are offline on your network.

  • Download the library into your static/ folder by running this command:
curl -o static/html5-qrcode.min.js https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js

Why download instead of using a CDN?

A CDN link requires every page load to reach the internet. Since your book scanner runs on your local network, downloading the file once means the app works even if your internet connection drops. Only the Open Library lookup needs connectivity.

  • Verify the file downloaded successfully by running:
ls -lh static/html5-qrcode.min.js

You should see the file listed with a size of approximately 375 KB. If you see a file with 0 bytes or an error, the download did not succeed.

File is 0 bytes or download failed?

Make sure you have an active internet connection. Try running the curl command again. If curl is not installed, use wget instead:

wget -O static/html5-qrcode.min.js https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js

Help me download html5-qrcode.min.js.

Open the project in VS Code
  • Open the book-scanner folder in VS Code by running:
code .

VS Code opens with your project folder in the sidebar. You should see app.py, the static/ folder containing html5-qrcode.min.js, and the empty templates/ folder.

Your project skeleton is ready. Next up, you will write the database code and build your first web page so you can see your app running in a browser.

Build the Database and Book List

Your project folder is ready with Flask installed and all the directories in place. Now you need somewhere to actually store books and a way to see them.

Before barcode scanning can work, your app needs a database to hold book records and a web page to display them. In this step, you will build both from scratch using SQLite and Flask templates.

In this step, get ready to:
  • Write the SQLite database setup code that creates a books table.
  • Build the Flask route and HTML template that displays your collection.
  • Add an API endpoint for inserting books and an ISBN text input field.
Write the SQLite database setup code

Every book you scan needs to be stored somewhere persistent. SQLite is a database engine built into Python's standard library, so you do not need to install anything extra.

  • In VS Code, open app.py from the Explorer sidebar.
  • Add the following imports and Flask app setup by pasting this code:
import sqlite3
import requests
from flask import Flask, render_template, request, jsonify, g

# Create the Flask app and set the database filename
app = Flask(__name__)
DATABASE = "books.db"


def get_db():
    # Reuse one database connection per request
    if "db" not in g:
        g.db = sqlite3.connect(DATABASE)
        g.db.row_factory = sqlite3.Row
    return g.db


@app.teardown_appcontext
def close_db(exception):
    # Close the database connection when the request ends
    db = g.pop("db", None)
    if db is not None:
        db.close()

What does this code do?

You import sqlite3 (Python's built-in database module) and several Flask helpers.

  • DATABASE = "books.db" tells your app where to store the SQLite file. It will be created automatically on first run.
  • get_db() opens a database connection and stores it on Flask's g object so the same connection is reused within one request.
  • close_db() runs automatically at the end of every request to close the connection cleanly.

Now you need the function that actually creates the books table, plus the main block that starts the server.

  • Add the following code below close_db() in app.py:
def init_db():
    # Create the books table if it doesn't exist yet
    db = get_db()
    db.execute(
        """
        CREATE TABLE IF NOT EXISTS books (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            isbn TEXT UNIQUE,
            title TEXT NOT NULL,
            author TEXT,
            publisher TEXT,
            year TEXT,
            cover_url TEXT,
            added_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        """
    )
    db.commit()


if __name__ == "__main__":
    with app.app_context():
        init_db()
    app.run(host="0.0.0.0", port=5000, debug=True)

What does this code do?

init_db() runs a SQL statement that creates the books table with columns for ISBN, title, author, publisher, year, cover URL, and a timestamp.

The if __name__ == "__main__" block calls init_db() on startup, then launches the Flask development server on port 5000 accessible from any device on your network.

  • CREATE TABLE IF NOT EXISTS means the table is only created the first time. Restarting the app will not erase your data.
  • Save app.py.
  • Run your app to confirm the server starts and the database file is created:
python3 app.py

You should see output like * Running on all addresses (0.0.0.0) and * Running on http://127.0.0.1:5000.

  • Open a second terminal and confirm the database file was created by running:
ls books.db

You should see books.db listed. Your database is ready.

  • Switch back to the first terminal and press Ctrl+C to stop the server.

Server didn't start?

  • Make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
  • Check for typos in the import lines. A misspelled module name causes an ImportError immediately.
  • help me debug why my Flask server won't start
Build the Flask route and HTML template

The server starts, but visiting it in a browser gives a 404 error because there are no routes yet. You need a route that queries the database and passes the results to an HTML template.

  • In app.py, find the line that says if __name__ == "__main__": and add the following route above it:
@app.route("/")
def index():
    # Fetch all books, newest first
    db = get_db()
    books = db.execute("SELECT * FROM books ORDER BY added_date DESC").fetchall()
    return render_template("index.html", books=books)

What does this route do?

When someone visits the root URL (/), Flask runs index(). It queries all rows from the books table, sorted by newest first, and sends them to an HTML template called index.html.

Now create the HTML template that renders the book list.

  • In VS Code, create a new file inside the templates/ folder called index.html.
  • Add the following HTML head section with page styles:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Book Scanner</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
            max-width: 900px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
        }
        h1 { margin-bottom: 20px; color: #333; }
        h2 { margin-bottom: 15px; color: #555; }
        .book-list { list-style: none; }
        .book-item {
            background: white;
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 10px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            display: flex;
            align-items: center;
        }
        .book-item .info { flex: 1; }
        .book-item .info h3 { margin-bottom: 3px; font-size: 16px; }
        .book-item .info p { color: #666; font-size: 14px; }
        .empty-state { text-align: center; color: #999; padding: 40px; }
    </style>
</head>
  • Directly below the </head> tag, add the body content that renders your book collection:
<body>
    <h1>Book Scanner</h1>

    <h2>My Collection ({{ books|length }} books)</h2>
    {% if books %}
    <ul class="book-list">
        {% for book in books %}
        <li class="book-item">
            <div class="info">
                <h3>{{ book.title }}</h3>
                <p>{{ book.author }}</p>
                <p>{{ book.isbn }}</p>
            </div>
        </li>
        {% endfor %}
    </ul>
    {% else %}
    <div class="empty-state">
        <p>No books yet. Scan a barcode to get started!</p>
    </div>
    {% endif %}
</body>
</html>

What does this template do?

Flask uses Jinja2 templating to inject Python data into HTML. The {{ books|length }} expression displays the total count of books.

The {% if books %} block shows the book list when books exist. The {% else %} block displays the "No books yet." message when the database is empty.

  • Save templates/index.html.
  • Switch back to the terminal and start the server again:
python3 app.py
  • Open your browser and go to http://127.0.0.1:5000.

You should see the page title "Book Scanner", the heading "My Collection (0 books)", and the message "No books yet. Scan a barcode to get started!" displayed in the center.

Seeing a TemplateNotFound error?

  • Make sure your file is at templates/index.html (inside the templates folder, not the project root).
  • Check that the filename is exactly index.html with no extra spaces or different capitalization.
  • help me fix a TemplateNotFound error in my Flask app
Add the book API endpoint and ISBN input field

You can see the collection page, but there is no way to add books yet. You need an API endpoint that accepts book data and an input field where you can type or scan an ISBN.

  • Stop the server with Ctrl+C.
  • In app.py, find the line if __name__ == "__main__": and add this new route above it:
@app.route("/api/books", methods=["POST"])
def add_book():
    # Accept book data as JSON and insert into the database
    data = request.get_json()
    db = get_db()
    try:
        db.execute(
            "INSERT INTO books (isbn, title, author, publisher, year, cover_url) VALUES (?, ?, ?, ?, ?, ?)",
            (
                data.get("isbn", ""),
                data["title"],
                data.get("author", ""),
                data.get("publisher", ""),
                data.get("year", ""),
                data.get("cover_url", ""),
            ),
        )
        db.commit()
        return jsonify({"message": "Book added successfully"}), 201
    except sqlite3.IntegrityError:
        return jsonify({"error": "Book with this ISBN already exists"}), 409

What does this endpoint do?

This creates a POST endpoint at /api/books that accepts JSON data containing book details. It inserts the book into the database and returns a success message.

If a book with the same ISBN already exists, SQLite raises an IntegrityError because the isbn column is marked UNIQUE. The endpoint catches this and returns a 409 conflict response instead of crashing.

  • Save app.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py and move on to updating the template.

ⓧ I'd like to double check the full code

Scroll up and confirm your app.py contains these sections in order:

  • Imports and Flask app setup (import sqlite3 through close_db).
  • init_db() function with the CREATE TABLE statement.
  • index() route that queries books and renders index.html.
  • add_book() route at /api/books with the INSERT statement.
  • if __name__ == "__main__": block that calls init_db() and starts the server.

Now add the ISBN input field to the template so you have a place to type or scan barcodes.

  • In templates/index.html, find the closing </style> tag and add these styles just above it:
.scan-section {
    background: white;
    padding: 20px;
    border-radius: 8px;
    margin-bottom: 20px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#isbn-input {
    width: 100%;
    padding: 12px;
    font-size: 18px;
    border: 2px solid #ddd;
    border-radius: 4px;
    margin-bottom: 10px;
}
#isbn-input:focus { outline: none; border-color: #4a90d9; }
  • Now find the line <h2>My Collection in the body and add this scan section above it:
    <div class="scan-section">
        <h2>Scan or Enter ISBN</h2>
        <input type="text" id="isbn-input" placeholder="Scan barcode or type ISBN here..." autofocus>
    </div>

Why add the input now?

A USB barcode scanner acts as a keyboard. When you scan a barcode, it types the digits into whatever text field is focused. By placing this input field now, you are ready for the JavaScript connection in the next step.

  • Save templates/index.html.

✔️ Awesome, I've got everything!

Great. Double check you have saved templates/index.html and move on to the final verify.

ⓧ I'd like to double check the full code

Scroll up and confirm your templates/index.html contains these sections in order:

  • HTML head with all styles (body, .scan-section, #isbn-input, .book-list, .book-item, .empty-state).
  • The scan section div with the ISBN input field.
  • The "My Collection" heading with {{ books|length }}.
  • The Jinja2 {% if books %} block with book list items and the {% else %} empty state.

Before you restart the server, what do you expect to see when you refresh the page?

  • Start the server again:
python3 app.py
  • Refresh http://127.0.0.1:5000 in your browser.

You should now see a white card with the "Scan or Enter ISBN" input field at the top, followed by "My Collection (0 books)" and the message "No books yet. Scan a barcode to get started!" below it.

Page still looks the same as before?

  • Make sure you saved both app.py and templates/index.html before restarting.
  • Do a hard refresh in your browser (Ctrl+Shift+R or Cmd+Shift+R) to bypass any cached version.
  • help me figure out why my Flask template changes aren't showing up

Your database is live and your collection page renders perfectly. Next up, you will connect the ISBN input to the Open Library API so a single barcode scan fetches the full book record automatically.

Add ISBN Lookup with Open Library

Your app can display books and store them in the database, but right now the only way to add one is manually typing all the details.

In this step, you will connect your app to the Open Library free API so that a single ISBN gives you the full book record automatically. Your USB barcode scanner acts like a keyboard. It types the barcode digits into the focused input field and presses Enter. That means a simple event listener is all you need to trigger a lookup.

In this step, get ready to:
  • Build the ISBN lookup endpoint that calls the Open Library API.
  • Add JavaScript to display book details from an ISBN.
  • Wire up the confirm and delete buttons for collection management.
Build the ISBN lookup endpoint

Open Library provides a free API that returns edition-level data when you pass it an ISBN. You will add a new route to app.py that fetches this data and returns a clean JSON object your frontend can display.

  • In app.py, find the add_book() function.
  • Directly above it, add this new route that calls Open Library and resolves the author name:
@app.route("/api/lookup/<isbn>")
def lookup_isbn(isbn):
    try:
        response = requests.get(
            f"https://openlibrary.org/isbn/{isbn}.json",
            headers={"User-Agent": "BookScanner/1.0 (local)"},
            timeout=10,
        )
    except requests.RequestException:
        return jsonify({"error": "Could not connect to Open Library"}), 503

    if response.status_code != 200:
        return jsonify({"error": "Book not found"}), 404

    data = response.json()

    author = "Unknown"
    if "authors" in data and data["authors"]:
        author_key = data["authors"][0].get("key", "")
        if author_key:
            try:
                author_response = requests.get(
                    f"https://openlibrary.org{author_key}.json",
                    headers={"User-Agent": "BookScanner/1.0 (local)"},
                    timeout=10,
                )
                if author_response.status_code == 200:
                    author = author_response.json().get("name", "Unknown")
            except requests.RequestException:
                pass

What does this code do?

The route accepts any ISBN in the URL and makes an HTTP request to Open Library's ISBN endpoint.

  • The User-Agent header identifies your app to Open Library, which allows 3 requests per second for identified clients.
  • Open Library returns author data as a key (like /authors/OL123A), not the actual name. A second API call resolves that key into a readable author name.
  • If either request fails, the code gracefully falls back to "Unknown" instead of crashing.
  • Directly below the code you just added (still inside the lookup_isbn function), add the cover image lookup and the return statement:
    cover_url = ""
    if "covers" in data and data["covers"]:
        cover_id = data["covers"][0]
        cover_url = f"https://covers.openlibrary.org/b/id/{cover_id}-M.jpg"

    book_info = {
        "isbn": isbn,
        "title": data.get("title", "Unknown Title"),
        "author": author,
        "publisher": (
            data.get("publishers", ["Unknown"])[0]
            if data.get("publishers")
            else "Unknown"
        ),
        "year": data.get("publish_date", "Unknown"),
        "cover_url": cover_url,
    }

    return jsonify(book_info)

What does this code do?

This builds the final JSON response your frontend will display.

  • Cover images come from Open Library's covers API using the cover ID from the ISBN response.
  • The book_info dictionary collects everything into a clean shape: isbn, title, author, publisher, year, and cover_url.
  • Flask's jsonify() converts the dictionary into a proper JSON HTTP response.
  • Save app.py.
  • Test the endpoint by running your app and opening this URL in your browser:
python3 app.py

Before you open the test URL, what do you think the response will look like for a valid ISBN?

  • Open http://127.0.0.1:5000/api/lookup/9780140449136 in your browser.

You should see a JSON response with a title, author name, publisher, year, and a cover URL. The ISBN 9780140449136 is a Penguin Classics edition, so you will see real book metadata from Open Library.

Seeing an empty response or error?

  • Make sure your virtual environment is active and requests is installed.
  • Check that the new function is placed above add_book() and below the index() function.
  • Confirm your machine has internet access. Open Library is a remote API.

help me debug my Open Library lookup endpoint

  • Stop the server with Ctrl+C.
  • Now add a delete endpoint below add_book() so users can remove books from their collection:
@app.route("/api/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
    db = get_db()
    db.execute("DELETE FROM books WHERE id = ?", (book_id,))
    db.commit()
    return jsonify({"message": "Book deleted"}), 200

What does this code do?

This route accepts a DELETE request with a book ID in the URL. It removes that row from the books table so the book disappears from your collection on the next page load.

  • Save app.py.
Update the template with lookup and display logic

Your backend can now look up books and delete them. Next you need a frontend that triggers the lookup when an ISBN is entered, shows the result, and lets the user confirm or delete.

  • Open templates/index.html in VS Code.
  • Select all the existing content and replace it entirely with the code below, starting with the HTML head and styles:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Book Scanner</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
            max-width: 900px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
        }
        h1 { margin-bottom: 20px; color: #333; }
        h2 { margin-bottom: 15px; color: #555; }
        .scan-section {
            background: white;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        #isbn-input {
            width: 100%;
            padding: 12px;
            font-size: 18px;
            border: 2px solid #ddd;
            border-radius: 4px;
            margin-bottom: 10px;
        }
        #isbn-input:focus { outline: none; border-color: #4a90d9; }

What does this code do?

This sets up the page structure with a centered layout, a light grey background, and a white card for the scanning section. The ISBN input gets a large font so it is easy to read when barcode digits appear.

  • Directly below that code, add the remaining styles:
        .book-result {
            background: #e8f5e9;
            padding: 15px;
            border-radius: 8px;
            margin-top: 15px;
            display: none;
            overflow: hidden;
        }
        .book-result img {
            width: 80px;
            float: left;
            margin-right: 15px;
            border-radius: 4px;
        }
        .book-result h3 { margin-bottom: 5px; }
        .book-result p { color: #666; margin-bottom: 3px; }
        .btn {
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            margin-top: 10px;
        }
        .btn-add { background: #4caf50; color: white; }
        .btn-add:hover { background: #45a049; }
        .btn-delete { background: #f44336; color: white; font-size: 12px; padding: 5px 10px; }
        .btn-delete:hover { background: #d32f2f; }
  • Continue adding the book list styles and close the head section:
        .book-list { list-style: none; }
        .book-item {
            background: white;
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 10px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            display: flex;
            align-items: center;
        }
        .book-item img {
            width: 60px;
            height: 80px;
            object-fit: cover;
            border-radius: 4px;
            margin-right: 15px;
            background: #eee;
        }
        .book-item .info { flex: 1; }
        .book-item .info h3 { margin-bottom: 3px; font-size: 16px; }
        .book-item .info p { color: #666; font-size: 14px; }
        .no-cover {
            width: 60px;
            height: 80px;
            background: #ddd;
            border-radius: 4px;
            margin-right: 15px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 10px;
            color: #999;
        }
  • Add the status message styles and close the style and head tags:
        .status {
            padding: 10px;
            border-radius: 4px;
            margin-top: 10px;
            display: none;
        }
        .status.success { background: #e8f5e9; color: #2e7d32; display: block; }
        .status.error { background: #ffebee; color: #c62828; display: block; }
        .empty-state { text-align: center; color: #999; padding: 40px; }
    </style>
</head>
  • Now add the body HTML with the scan section and book result display:
<body>
    <h1>Book Scanner</h1>

    <div class="scan-section">
        <h2>Scan or Enter ISBN</h2>
        <input type="text" id="isbn-input" placeholder="Scan barcode or type ISBN here..." autofocus>
        <div id="status" class="status"></div>
        <div id="book-result" class="book-result">
            <img id="result-cover" src="" alt="Cover">
            <h3 id="result-title"></h3>
            <p id="result-author"></p>
            <p id="result-publisher"></p>
            <p id="result-year"></p>
            <br style="clear:both">
            <button class="btn btn-add" onclick="confirmAdd()">Add to Collection</button>
        </div>
    </div>

What does this HTML do?

The isbn-input field is where your USB scanner types the barcode digits. The autofocus attribute means it is selected as soon as the page loads, so the scanner always has somewhere to type.

The book-result div starts hidden (display: none). Your JavaScript will reveal it after a successful lookup, showing the cover image and metadata.

  • Below the scan section, add the book collection list with Jinja2 templating:
    <h2>My Collection ({{ books|length }} books)</h2>
    {% if books %}
    <ul class="book-list">
        {% for book in books %}
        <li class="book-item">
            {% if book.cover_url %}
            <img src="{{ book.cover_url }}" alt="Cover">
            {% else %}
            <div class="no-cover">No Cover</div>
            {% endif %}
            <div class="info">
                <h3>{{ book.title }}</h3>
                <p>{{ book.author }}</p>
                <p>{{ book.isbn }}</p>
            </div>
            <button class="btn btn-delete" onclick="deleteBook({{ book.id }})">Delete</button>
        </li>
        {% endfor %}
    </ul>
    {% else %}
    <div class="empty-state">
        <p>No books yet. Scan a barcode to get started!</p>
    </div>
    {% endif %}

What does this code do?

Jinja2 loops through all books from the database and renders each as a card with a cover image, title, author, ISBN, and a delete button. If the collection is empty, it shows the "No books yet" message instead.

  • Now add the JavaScript that listens for Enter on the ISBN input and calls your lookup endpoint:
    <script>
        let currentBook = null;

        const isbnInput = document.getElementById("isbn-input");

        // Listen for Enter key (triggered by USB scanner or manual input)
        isbnInput.addEventListener("keypress", function(e) {
            if (e.key === "Enter") {
                const isbn = this.value.trim().replace(/-/g, "");
                if (isbn.length >= 10) {
                    lookupISBN(isbn);
                    this.value = "";
                }
            }
        });

        // Fetch book details from our lookup endpoint
        function lookupISBN(isbn) {
            showStatus("Looking up ISBN: " + isbn + "...", "success");
            document.getElementById("book-result").style.display = "none";

            fetch("/api/lookup/" + isbn)
                .then(function(response) {
                    if (!response.ok) throw new Error("Book not found");
                    return response.json();
                })
                .then(function(data) {
                    currentBook = data;
                    document.getElementById("result-title").textContent = data.title;
                    document.getElementById("result-author").textContent = "by " + data.author;

What does this code do?

  • The keypress listener waits for Enter. USB barcode scanners send Enter after typing all the digits, so this triggers the lookup automatically.
  • The replace(/-/g, "") strips hyphens in case someone types an ISBN with dashes.
  • The lookupISBN() function calls your Flask endpoint, then fills in the book-result div with the returned data.
Wire the confirm and delete buttons

The lookup displays results, but the user needs to confirm before a book is added. You also need to close the lookupISBN function's promise chain and add the remaining functions.

  • Directly below the last line you added, continue the JavaScript with the rest of the lookup display and all remaining functions:
                    document.getElementById("result-publisher").textContent = data.publisher;
                    document.getElementById("result-year").textContent = data.year;
                    document.getElementById("result-cover").src = data.cover_url || "";
                    document.getElementById("book-result").style.display = "block";
                    hideStatus();
                })
                .catch(function(error) {
                    showStatus("Could not find book with ISBN: " + isbn, "error");
                });
        }

        // Save the looked-up book to the database
        function confirmAdd() {
            if (!currentBook) return;
            fetch("/api/books", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify(currentBook)
            })
            .then(function(response) {
                if (response.status === 409) throw new Error("Book already in collection");
                if (!response.ok) throw new Error("Failed to add book");
                return response.json();
            })
            .then(function(data) {
                showStatus("Added: " + currentBook.title, "success");
                document.getElementById("book-result").style.display = "none";
                currentBook = null;
                setTimeout(function() { location.reload(); }, 1000);
            })
  • Finally, add the last portion of the script to close confirmAdd and add the delete and status helper functions:
            .catch(function(error) {
                showStatus(error.message, "error");
            });
        }

        // Remove a book from the collection
        function deleteBook(bookId) {
            if (!confirm("Remove this book from your collection?")) return;
            fetch("/api/books/" + bookId, { method: "DELETE" })
                .then(function() { location.reload(); });
        }

        function showStatus(message, type) {
            var el = document.getElementById("status");
            el.textContent = message;
            el.className = "status " + type;
        }

        function hideStatus() {
            document.getElementById("status").className = "status";
        }
    </script>
</body>
</html>

What do these functions do?

  • confirmAdd() POSTs the looked-up book to your /api/books endpoint, saves it to the database, then reloads the page after 1 second so the new book appears in the list.
  • deleteBook() shows a confirmation dialog, then sends a DELETE request and reloads the page to remove the book from view.
  • showStatus() and hideStatus() toggle a coloured banner that gives the user feedback during lookups and errors.
  • Save templates/index.html.
  • Restart your Flask server:
python3 app.py

Before you test, what do you think will happen when you type an ISBN into the input and press Enter?

  • Open http://127.0.0.1:5000 in your browser.
  • Type 9780140449136 into the ISBN input field and press Enter.

You should see a green result card appear with the book's title, author, publisher, year, and a cover image. Click Add to Collection and the page reloads with the book now visible in your collection list, complete with its cover art.

USB barcode scanner shortcut

If you have a USB barcode scanner, plug it in and scan any book. The scanner types the digits into the focused input and presses Enter automatically. The lookup triggers instantly with no extra setup required.

Book not appearing after clicking Add?

  • Check the browser console for errors. If you see a 409 status, the book is already in your collection from an earlier test.
  • Make sure app.py has the delete_book route added below add_book. If you added it in the wrong place, the server may not start.
  • Confirm the book-result div IDs in your HTML match what the JavaScript references: result-title, result-author, result-cover.

help me debug my book lookup and collection flow

✔️ Awesome, I've got everything!

Great. Double check you have saved both app.py and templates/index.html.

ⓧ I'd like to double check the full code

Your app.py should contain the following functions in this order: get_db(), close_db(), init_db(), index(), lookup_isbn(), add_book(), delete_book(), and the if __name__ block.

Scroll up and confirm each of the following chunks is present in templates/index.html:

  • HTML head with all CSS styles (scan-section, book-result, btn, book-item, no-cover, status, empty-state classes).
  • Body with scan section containing isbn-input, status div, and book-result div.
  • Book list with Jinja2 loop, delete buttons, and empty state fallback.
  • Script tag with lookupISBN(), confirmAdd(), deleteBook(), showStatus(), and hideStatus().

Your app now fetches real book data from a single ISBN and saves it to your local database. Next up, you will add phone camera scanning so you can catalog books without a USB scanner.

Add Phone Camera Scanning with HTTPS

Your USB barcode scanner works great from your computer because it just types digits into a focused text field. But a web app can do better. You can scan books from your phone too, using its built-in camera.

In this step, you will wire up the html5-qrcode library for phone camera scanning. You will also discover why browsers block camera access over plain HTTP and fix it with a self-signed SSL certificate.

In this step, get ready to:
  • Add a camera scanner toggle button and scanning logic to your template.
  • Generate a self-signed SSL certificate for HTTPS.
  • Test phone camera scanning from any device on your local network.
Add the html5-qrcode camera scanner to the template

The html5-qrcode library runs entirely in the browser. It accesses the device camera, processes video frames, and decodes barcodes client-side. Your server never touches image data.

  • In templates/index.html, find the <style> block. Add these rules at the end, just before the closing </style> tag:
/* Camera scanner button and reader area */
#reader { width: 100%; max-width: 400px; margin: 10px auto; }
.btn-camera { background: #2196f3; color: white; margin-bottom: 10px; }
.btn-camera:hover { background: #1976d2; }
  • Now find the <input type="text" id="isbn-input" line in the HTML body. Add the camera button and reader div directly after it:
<button class="btn btn-camera" id="camera-toggle" onclick="toggleCamera()">Start Camera Scanner</button>
<div id="reader"></div>

What do these elements do?

The #reader CSS constrains the camera viewfinder to a reasonable width and centres it. The .btn-camera rules give the toggle button a blue colour that visually separates it from the green "Add to Collection" button.

The empty <div id="reader"> is where html5-qrcode renders its camera viewfinder when activated. The button's onclick calls toggleCamera() which you will write next.

  • Save templates/index.html and refresh your browser at http://127.0.0.1:5000.
  • You should see a blue "Start Camera Scanner" button below the ISBN input field.

Don't see the blue button?

  • Make sure you placed the button after the isbn-input element and before the status div.
  • Check that the .btn-camera CSS rule is inside the <style> block, not outside it.

Help me debug why my camera button is not showing up.

The button is visible, but clicking it does nothing yet. You need the JavaScript that controls the camera scanner.

  • In templates/index.html, find let currentBook = null; inside the <script> tag. Add these two variable declarations directly after it:
let html5QrcodeScanner = null;
let cameraActive = false;
  • Now add the toggleCamera() function at the bottom of the script, after your hideStatus() function and before the closing </script> tag:
function toggleCamera() {
    if (cameraActive) {
        if (html5QrcodeScanner) {
            html5QrcodeScanner.clear();
            html5QrcodeScanner = null;
        }
        cameraActive = false;
        document.getElementById("camera-toggle").textContent = "Start Camera Scanner";
    } else {
        html5QrcodeScanner = new Html5QrcodeScanner(
            "reader",
            {
                fps: 10,
                qrbox: { width: 250, height: 150 },
                formatsToSupport: [
                    Html5QrcodeSupportedFormats.EAN_13,
                    Html5QrcodeSupportedFormats.EAN_8,
                    Html5QrcodeSupportedFormats.UPC_A,
                    Html5QrcodeSupportedFormats.UPC_E
                ]
            },
            false
        );
        html5QrcodeScanner.render(onScanSuccess);
        cameraActive = true;
        document.getElementById("camera-toggle").textContent = "Stop Camera Scanner";
    }
}

What does toggleCamera do?

  • If the camera is already active, it calls .clear() to stop the scanner and resets the button text.
  • If the camera is off, it creates a new Html5QrcodeScanner targeting the #reader div and starts scanning at 10 frames per second.
  • The formatsToSupport array limits detection to book barcode formats (EAN-13, EAN-8, UPC-A, UPC-E), which speeds up recognition.
  • Save templates/index.html and refresh your browser.
  • Click the "Start Camera Scanner" button. Your browser asks for camera permission. Allow it, and you should see a live camera viewfinder appear on the page.

Camera permission not appearing?

  • Make sure you are accessing the page via 127.0.0.1 or localhost. Browsers treat these as secure contexts and allow camera access without HTTPS.
  • Check the browser console for JavaScript errors. A typo in Html5QrcodeSupportedFormats will cause a silent failure.

Why is the camera scanner not activating when I click the button?

The camera is live, but scanning a barcode does nothing yet. You need a callback that receives the decoded ISBN and triggers a lookup.

  • Add the onScanSuccess() function directly after toggleCamera() in your script:
function onScanSuccess(decodedText, decodedResult) {
    var isbn = decodedText.trim().replace(/-/g, "");
    if (isbn.length >= 10) {
        // Stop the camera after a successful scan
        if (html5QrcodeScanner) {
            html5QrcodeScanner.clear();
            html5QrcodeScanner = null;
        }
        cameraActive = false;
        document.getElementById("camera-toggle").textContent = "Start Camera Scanner";
        lookupISBN(isbn);
    }
}

What does onScanSuccess do?

When the scanner reads a barcode, it extracts the digits, strips dashes, and checks if the result is at least 10 characters (the minimum valid ISBN length). It then stops the camera to save resources and calls lookupISBN() to fetch the book details from Open Library.

  • Save templates/index.html and refresh your browser.
  • Click "Start Camera Scanner", then hold a book barcode in front of your webcam. The camera should stop, and the book's title and author should appear in the green result area.

Camera does not detect the barcode?

  • Hold the barcode steady about 15-20 cm from the camera. Make sure the full barcode is visible inside the scanning box.
  • If you do not have a physical book handy, display a barcode image on another screen and point your webcam at it.

Help me get barcode detection working with html5-qrcode.

Camera scanning works on your computer. Now try accessing the app from your phone to see if it works there too.

  • Find your computer's local IP address by running this command:
hostname -I
  • Note the first IP address printed (it looks like 192.168.x.x).
  • On your phone, open a browser and go to http:// followed by that IP address, then :5000 (for example http://192.168.1.50:5000).

Before you tap "Start Camera Scanner" on your phone, what do you think will happen? Will the camera activate the same way it did on your computer?

  • Tap "Start Camera Scanner" on your phone.

The camera does not activate. The scanner UI may appear, but the phone never asks for camera permission. This is the expected result.

Why does the camera work on localhost but not from your phone?

Browsers enforce a rule called Secure Contexts. Camera access requires either localhost (which is always trusted) or an HTTPS connection. When your phone connects over plain HTTP to your computer's IP address, the browser treats it as insecure and blocks the camera API entirely.

The fix is to serve your app over HTTPS using a self-signed SSL certificate.

Generate a self-signed SSL certificate and serve over HTTPS

A self-signed certificate is not trusted by certificate authorities, so browsers show a warning. But it does enable HTTPS, which unlocks camera access from network devices.

  • Stop the Flask server with Ctrl+C in your terminal.
  • Generate the certificate and private key by running this command from your book-scanner directory:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=bookscanner"

What does this command do?

  • openssl req -x509 generates a self-signed X.509 certificate (no certificate authority needed).
  • -newkey rsa:2048 creates a new 2048-bit RSA private key.
  • -keyout key.pem -out cert.pem saves the key and certificate to files in your project folder.
  • -nodes means no passphrase, so Flask can load the key without prompting you.
  • -days 365 keeps the certificate valid for one year.
  • Confirm the files were created by running:
ls *.pem
  • You should see cert.pem and key.pem in the output.

Command not found?

The openssl tool comes pre-installed on Ubuntu. If you see an error, make sure you are running the command inside your book-scanner project directory with your virtual environment activated.

Help me generate an SSL certificate for my Flask app.

Now tell Flask to use these certificate files when starting the server.

  • In app.py, scroll to the very last line. Find this line:
    app.run(host="0.0.0.0", port=5000, debug=True)
  • Replace it with this line, adding the ssl_context parameter:
    app.run(host="0.0.0.0", port=5000, ssl_context=("cert.pem", "key.pem"), debug=True)

What does ssl_context do?

The ssl_context parameter tells Flask's built-in Werkzeug server to serve over HTTPS using your certificate and key files. Every connection is now encrypted, and browsers recognise the page as a secure context that can access the camera.

  • Save app.py and restart the server by running:
python3 app.py
  • You should see output that includes Running on https://0.0.0.0:5000 instead of the previous http:// version.

Seeing a file not found error?

  • Make sure cert.pem and key.pem are in the same directory as app.py. Run ls to confirm both files are there.
  • Check that the filenames in ssl_context match exactly: "cert.pem" and "key.pem".

Help me fix an SSL certificate error when starting my Flask app.

Test the full flow from your phone

Your app now serves over HTTPS. Time to prove that phone camera scanning works end-to-end.

  • On your computer, open https://127.0.0.1:5000 in your browser.
  • You will see a certificate warning because the certificate is self-signed. Click Advanced, then Proceed to continue.
  • On your phone, open your browser and go to https:// followed by your computer's IP address and :5000 (for example https://192.168.1.50:5000).
  • Accept the certificate warning on your phone the same way you did on your computer.
  • Tap "Start Camera Scanner" on your phone.

This time your phone asks for camera permission. Allow it, and you see a live camera viewfinder.

  • Point your phone camera at any book barcode.
  • The scanner reads the ISBN, the camera stops, and the book's title, author, and cover image appear on screen.
  • Tap Add to Collection to save the book. The page reloads and the book appears in your collection list.

✔️ Awesome, I've got everything!

Great. Double check you have saved both app.py and templates/index.html.

ⓧ I'd like to double check the full code

Scroll up and confirm these pieces are all present in your templates/index.html:

  • The #reader and .btn-camera CSS rules in the style block.
  • The <button class="btn btn-camera" id="camera-toggle"> and <div id="reader"></div> after the ISBN input.
  • The html5QrcodeScanner and cameraActive variable declarations after currentBook.
  • The toggleCamera() function with Html5QrcodeScanner and four format types.
  • The onScanSuccess() function that calls lookupISBN().

In app.py, confirm the last line reads:

    app.run(host="0.0.0.0", port=5000, ssl_context=("cert.pem", "key.pem"), debug=True)

Your book scanner is now a fully network-accessible app. Any device on your WiFi can scan books with its camera. Next up, you will tackle the Secret Mission and make your growing collection searchable.

Secret mission

Add Live Search to Your Collection

Once your collection grows past a dozen books, scrolling to find one is tedious. Can you add a search bar that filters your collection as you type, matching against both title and author?

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 entirely locally, so there are no ongoing costs.

Resources you used:

  • book-scanner/ project directory containing app.py, templates/, static/, and your Python virtual environment.
  • books.db SQLite database file storing your book collection (inside the project directory).
  • cert.pem and key.pem self-signed SSL certificate files (inside the project directory).
  • The Flask development server process (runs only while your terminal is open).

Keep everything running

No action needed. Choose this if you are still actively building or scanning books into your collection.

  • Leave the terminal open with python3 app.py running to keep the app accessible from any device on your network.
  • Your book data persists in books.db even if you restart the server.
  • The app uses no external paid services. Everything stays on your machine at zero cost.

Pause - I'll come back to this later

Shut down the running server to free up your terminal. Your data and files remain intact for next time.

  • Press Ctrl+C in the terminal where the Flask server is running.
  • Your book collection stays saved in books.db. Nothing is lost.
  • To restart later, activate your virtual environment and run python3 app.py from the book-scanner/ directory.

Delete - I don't want to use this again

Remove all project files and start fresh. All data is contained in one folder.

  • Press Ctrl+C in the terminal to stop the Flask server if it is still running.
  • Navigate to the parent directory (your Desktop) by running:
cd ~/Desktop
  • Delete the entire project directory by running:
rm -rf book-scanner/
  • Confirm the folder is gone by running:
ls

You should no longer see book-scanner/ in the output. This removes your virtual environment, database, SSL certificates, and all project code in one step.

Nice Work!

Nice Work!

Nice work! You just built a fully functional book collection database that runs on your local network, complete with barcode scanning, automatic metadata retrieval, and phone camera support.

You've learned how to:

  • Build a Flask web application from scratch that serves HTML pages and REST API endpoints, backed by a SQLite database.
  • Integrate the Open Library API to automatically fetch book metadata from ISBN numbers, turning a single barcode scan into a complete catalog entry.
  • Set up HTTPS with a self-signed certificate and serve the app on all network interfaces, enabling phone camera scanning from any device on your WiFi.
  • Secret Mission: Added live client-side search filtering that instantly narrows your collection as you type.

Ready to quiz yourself?