Build a Local Book Library App
Build a Flask web app that catalogs your books with cover images and locations.
Introduction
30 Second Summary
You know you own that book somewhere, but which shelf is it on? Keeping track of a home library gets harder as your collection grows.
In this project, you will build a personal book library web app that fetches real cover images and runs on your local network. You can pull it up from your phone while standing at a bookshelf to check if you already own a title.
What You'll Build
When you open your library in a browser, you see a clean grid of book covers alongside each book's physical location in your home, all fetched and stored automatically when you add a new book.
By the end of this project, you'll have:
- A running Flask web app that displays your books as a visual grid of cover images fetched automatically from the BookCover API.
- A persistent SQLite database storing each book's title, author, ISBN, and physical location in your home.
- LAN access so you can open the library from your phone, tablet, or any device on your home network.
- Secret Mission: Add real-time search and filter to find books by title, author, or location without reloading the page.
Are there any prerequisites?
None. This guide walks you through installing everything you need, including Python packages and the Flask framework. No prior web development experience is required.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
You're about to create a personal book library web app that fetches real cover images and lets you browse your collection from any device on your home network.
Get a Basic Flask App Running
In the previous step, you installed Flask and its dependencies into an isolated virtual environment. Now you have the tools. But tools alone don't prove anything works.
Before building any library features, you need a working Flask app that starts a server and renders a page. This is the foundation everything else builds on.
In this step, get ready to:
- Create app.py with a minimal Flask application and one route.
- Create an HTML template that displays the library title and an empty state.
- Run the Flask development server and confirm the page loads in your browser.
Create the Flask application file
Every Flask app starts with a Python file that creates the app instance and defines routes. Routes tell Flask which function to run when a browser visits a specific URL.
- In VS Code, hover over the Explorer sidebar and click the New File icon (a page with a plus sign).
- Name the file app.py and press Enter.
- Paste the following code into app.py:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html', books=[])
if __name__ == '__main__':
app.run(debug=True)
What does this code do?
- from flask import Flask, render_template imports the Flask class and the function that loads HTML templates.
- app = Flask(__name__) creates your Flask application instance. Flask uses __name__ to know where to find your project files.
- @app.route('/') maps the homepage URL to the index() function. When someone visits /, Flask calls this function.
- render_template('index.html', books=[]) loads an HTML file and passes an empty list of books to it.
- Save app.py with Ctrl+S.
- In the Explorer sidebar, confirm you see app.py listed at the top level of your project folder.
Don't see app.py in the sidebar?
- Make sure you saved the file. The dot on the tab disappears after saving.
- Check that VS Code is open in your project folder, not a parent folder. The file should appear at the top level.
Help me figure out why my app.py file isn't showing up in VS Code.
Create the HTML template
Flask looks for HTML templates in a folder named templates next to your application file. You need to create this folder and add your first template inside it.
- Create the templates folder by running this command in your terminal:
mkdir templates
- Confirm the folder was created by running:
ls
You should see templates listed alongside app.py in the output.
- In the VS Code Explorer sidebar, right-click on the templates folder and select New File.
- Name the file index.html and press Enter.
- Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Book Library</title>
</head>
<body>
<h1>My Book Library</h1>
<p class="empty-state">No books yet. Add your first book!</p>
</body>
</html>
What does this template do?
This is a simple HTML page with a heading and a paragraph. The empty-state class on the paragraph is a label for now. Later, you will use template logic to show this message only when there are no books in the library.
- Save index.html with Ctrl+S.
- In the Explorer sidebar, expand the templates folder and confirm index.html appears inside it.
Don't see index.html inside templates?
- Make sure you created the file inside the templates folder, not at the project root. If it ended up in the wrong place, drag it into templates in the sidebar.
- Check that the folder is named exactly templates (plural, lowercase). Flask will not find it otherwise.
Help me figure out why Flask cannot find my template file.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is app.py:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html', books=[])
if __name__ == '__main__':
app.run(debug=True)
Here is templates/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Book Library</title>
</head>
<body>
<h1>My Book Library</h1>
<p class="empty-state">No books yet. Add your first book!</p>
</body>
</html>
Run the Flask development server
Your app has a route and a template. Time to see if it actually works.
Before you run the next command, what do you think will happen? Will the server start successfully, or will you see an error?
- Start the Flask development server by running this command:
flask run
You should see a message indicating the server is running on http://127.0.0.1:5000.
Why does flask run find your app automatically?
Flask auto-discovers your application because the file is named app.py. This is a convention that lets you skip the --app flag. The server is now listening on port 5000 and waiting for browser requests.
- Open your browser and go to http://127.0.0.1:5000.
You should see a page with "My Book Library" as the heading and "No books yet. Add your first book!" below it.
Seeing an error or a blank page?
- If the terminal shows an error about the address already being in use, another program is using port 5000. Run flask run --port 5001 instead, then visit http://127.0.0.1:5001.
- If the browser shows a TemplateNotFound error, Flask cannot locate index.html. Double-check that the file is inside a folder named exactly templates (not "template" or "Templates").
- Make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt. If not, activate it and try flask run again.
Help me debug why my Flask app is not loading in the browser.
Your Flask app is alive. You have a running server, a route, and a template rendering a real page. Next up, you will add a form to enter books and a database to store them.
Add Books with a Form and Store in SQLite
In the last step, you got Flask running and confirmed your library page loads in the browser. Now you need actual books in it.
In this step, you will add a form for entering book details, store each book in a SQLite database, and display saved books on the library page. SQLite is built into Python's standard library, so there is nothing extra to install.
In this step, get ready to:
- Add database setup and a new route to app.py.
- Create an HTML form page for adding books.
- Update the library page to display saved books as a list.
Add the database and routes to your app
Your current app.py has a single route that renders a static page. You need to add database functions and a second route that accepts form submissions.
- In VS Code, open app.py and replace its entire contents with the following database setup code:
import sqlite3
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
DATABASE = 'books.db'
# Connect to the SQLite database file
def get_db():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
# Create the books table if it doesn't exist yet
def init_db():
conn = get_db()
conn.execute(
"""
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT NOT NULL,
isbn TEXT,
location TEXT NOT NULL,
cover_url TEXT
)
"""
)
conn.commit()
conn.close()
What does this code do?
get_db() opens a connection to a file called books.db. Setting row_factory = sqlite3.Row lets you access columns by name instead of index number.
init_db() creates the books table with columns for the book's title, author, ISBN, physical location, and a cover URL you will use later. The IF NOT EXISTS clause means it won't crash if the table already exists.
- Now add the routes and startup logic directly below the init_db function in the same file:
# Home page: fetch all books and pass them to the template
@app.route('/')
def index():
conn = get_db()
books = conn.execute('SELECT * FROM books ORDER BY id DESC').fetchall()
conn.close()
return render_template('index.html', books=books)
# Add book page: show form on GET, save book on POST
@app.route('/add', methods=['GET', 'POST'])
def add_book():
if request.method == 'POST':
title = request.form['title']
author = request.form['author']
isbn = request.form.get('isbn', '')
location = request.form['location']
conn = get_db()
conn.execute(
'INSERT INTO books (title, author, isbn, location, cover_url) VALUES (?, ?, ?, ?, ?)',
(title, author, isbn, location, '')
)
conn.commit()
conn.close()
return redirect(url_for('index'))
return render_template('add.html')
if __name__ == '__main__':
init_db()
app.run(debug=True)
What does this code do?
The / route queries all books from the database and passes them to the template so the page can display them.
The /add route handles two scenarios. A GET request renders the empty form. A POST request reads the form fields, inserts a new row into the database with an empty cover_url, and redirects back to the home page.
The if __name__ == '__main__' block now calls init_db() before starting the server, so the database and table are created on first run.
- Save app.py.
- In your terminal, stop the running server with Ctrl+C and start it again by running:
python app.py
You should see Flask's startup output with no errors, confirming the new code is valid.
Seeing a SyntaxError or IndentationError?
Make sure you deleted the old index() function and if __name__ block before pasting the new code. Duplicate function definitions or misaligned indentation are the most common causes.
help me fix a SyntaxError in my Flask app.py file
✔️ Awesome, I've got everything!
Great. Make sure you have saved app.py before continuing.
ⓧ I'd like to double check the full code
Your app.py should contain two sections in order:
- The database setup chunk (imports, DATABASE constant, get_db(), and init_db()).
- The routes chunk (index(), add_book(), and the if __name__ == '__main__' block).
Scroll up and confirm both chunks are present in that order with no leftover code from the previous version.
Create the add book form
The /add route expects a template called add.html. This page will show a form with fields for title, author, ISBN, and location.
- Create a new file called add.html inside your templates folder and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Book - My Book Library</title>
</head>
<body>
<div class="form-container">
<h1>Add a Book</h1>
<form method="POST" action="{{ url_for('add_book') }}">
<label for="title">Title</label>
<input type="text" id="title" name="title" required placeholder="e.g. The Hitchhiker's Guide to the Galaxy">
<label for="author">Author</label>
<input type="text" id="author" name="author" required placeholder="e.g. Douglas Adams">
<label for="isbn">ISBN (optional)</label>
<input type="text" id="isbn" name="isbn" placeholder="e.g. 978-0345376596">
<p class="hint">ISBN-13 helps find the cover image. Leave blank to search by title and author.</p>
<label for="location">Location</label>
<input type="text" id="location" name="location" required placeholder="e.g. Living Room, Top Shelf">
<button type="submit">Add Book</button>
</form>
<a href="{{ url_for('index') }}">Back to Library</a>
</div>
</body>
</html>
What does this form do?
The form's action points to the add_book route with method="POST". When the learner clicks the submit button, Flask receives the field values through request.form.
The required attribute on title, author, and location means the browser will not let the form submit if those fields are empty. ISBN is optional because not every book has one handy.
- Save templates/add.html.
- In your browser, visit http://127.0.0.1:5000/add.
You should see a plain form with four fields (Title, Author, ISBN, Location) and an "Add Book" button.
Seeing a TemplateNotFound error?
Make sure add.html is inside the templates folder, not in the project root. Flask only looks for templates in a folder named exactly templates.
help me fix a TemplateNotFound error in Flask
✔️ Awesome, I've got everything!
Great. You should see the form loading at /add.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Book - My Book Library</title>
</head>
<body>
<div class="form-container">
<h1>Add a Book</h1>
<form method="POST" action="{{ url_for('add_book') }}">
<label for="title">Title</label>
<input type="text" id="title" name="title" required placeholder="e.g. The Hitchhiker's Guide to the Galaxy">
<label for="author">Author</label>
<input type="text" id="author" name="author" required placeholder="e.g. Douglas Adams">
<label for="isbn">ISBN (optional)</label>
<input type="text" id="isbn" name="isbn" placeholder="e.g. 978-0345376596">
<p class="hint">ISBN-13 helps find the cover image. Leave blank to search by title and author.</p>
<label for="location">Location</label>
<input type="text" id="location" name="location" required placeholder="e.g. Living Room, Top Shelf">
<button type="submit">Add Book</button>
</form>
<a href="{{ url_for('index') }}">Back to Library</a>
</div>
</body>
</html>
Update the library page to show books
The home page still shows the hardcoded empty state. You need to update it to loop through books from the database and display them.
- Open templates/index.html and replace its entire contents with:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Book Library</title>
</head>
<body>
<h1>My Book Library</h1>
<a href="{{ url_for('add_book') }}">+ Add Book</a>
{% if books %}
<ul>
{% for book in books %}
<li>{{ book.title }} by {{ book.author }} — {{ book.location }}</li>
{% endfor %}
</ul>
{% else %}
<p class="empty-state">No books yet. Add your first book!</p>
{% endif %}
</body>
</html>
What changed in this template?
The template now has a link to the /add page and uses Jinja's {% if books %} block to check whether any books exist. If they do, it loops through each one and displays it as a list item. If the list is empty, it falls back to the original "No books yet" message.
- Save templates/index.html.
Before you test, what do you think will happen when you submit a book through the form? Will you see a cover image or just text?
- In your browser, go to http://127.0.0.1:5000 and click the + Add Book link.
- Enter The Hitchhiker's Guide to the Galaxy in the Title field.
- Enter Douglas Adams in the Author field.
- Enter 978-0345376596 in the ISBN field.
- Enter Living Room, Top Shelf in the Location field.
- Click Add Book.
The page redirects to the home page, and you see your book listed as plain text in a bullet point. No cover image, no visual card. Just text.
Don't see the book in the list?
Make sure your server is still running. If you restarted it with flask run instead of python app.py, the database might not have been initialized. Stop the server and run python app.py to ensure init_db() is called.
help me debug why my book is not showing up after submitting the form
✔️ Awesome, I've got everything!
Great. You should see your book as a plain text bullet on the home page.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Book Library</title>
</head>
<body>
<h1>My Book Library</h1>
<a href="{{ url_for('add_book') }}">+ Add Book</a>
{% if books %}
<ul>
{% for book in books %}
<li>{{ book.title }} by {{ book.author }} — {{ book.location }}</li>
{% endfor %}
</ul>
{% else %}
<p class="empty-state">No books yet. Add your first book!</p>
{% endif %}
</body>
</html>
Your library now stores books and survives a server restart. Next up, you will fetch real cover images from the BookCover API to transform this plain text list into a visual bookshelf.
Fetch Cover Images from the BookCover API
Your book list works. You can add books, persist them, and see them after a restart. But a plain bullet list of titles does not look or feel like a library.
The BookCover API from Longitood can fetch real cover images using an ISBN or a title-and-author search. In this step, you'll connect to that API, wire it into the add-book flow, and redesign the page into a visual grid of book cards.
In this step, get ready to:
- Add a function that fetches cover images from the BookCover API.
- Wire the cover fetch into your add-book route.
- Redesign the library page as a CSS grid of book cards with cover images.
Add the fetch_cover function
The BookCover API accepts a GET request with either an ISBN or a title-plus-author pair. It returns JSON containing a cover image URL. Your function needs to call that endpoint and handle failures gracefully so the app never crashes if a cover is unavailable.
- In app.py, find the line import sqlite3 at the top of the file.
- Add import requests on a new line directly below it:
import requests
- Now scroll down to the init_db() function. Add the following fetch_cover function directly below init_db(), leaving one blank line between them:
def fetch_cover(title, author, isbn):
try:
if isbn:
# Strip dashes from the ISBN for the API
clean_isbn = isbn.replace("-", "")
response = requests.get(
"https://bookcover.longitood.com/bookcover",
params={"isbn": clean_isbn},
timeout=10,
)
else:
# Fall back to searching by title and author
response = requests.get(
"https://bookcover.longitood.com/bookcover",
params={"book_title": title, "author_name": author},
timeout=10,
)
if response.status_code == 200:
data = response.json()
return data.get("url", "")
except (requests.RequestException, ValueError):
pass
return ""
What does this function do?
The function tries two paths to find a cover image. If the caller provides an ISBN, it strips dashes and queries the API with that ISBN. If no ISBN is available, it falls back to a title-and-author search.
The entire call is wrapped in a try/except block. If the API is down, the request times out, or the response is not valid JSON, the function returns an empty string instead of crashing.
- Save app.py.
Wire fetch_cover into the /add route
The function exists but nothing calls it yet. You need to call fetch_cover inside the /add route so that every new book gets a cover URL before it is saved to the database.
- In app.py, scroll down to the add_book function. Find the if request.method == 'POST': block.
- Replace everything inside that if block (from title = request.form['title'] down to return redirect(url_for('index'))) with the following code:
title = request.form["title"]
author = request.form["author"]
isbn = request.form.get("isbn", "")
location = request.form["location"]
# Fetch cover image from the BookCover API
cover_url = fetch_cover(title, author, isbn)
conn = get_db()
conn.execute(
"INSERT INTO books (title, author, isbn, location, cover_url) VALUES (?, ?, ?, ?, ?)",
(title, author, isbn, location, cover_url),
)
conn.commit()
conn.close()
return redirect(url_for("index"))
What changed here?
The key addition is the line cover_url = fetch_cover(title, author, isbn). This calls the function you just wrote, passing in the form data. The returned URL (or empty string on failure) is then stored alongside the book in the database.
Previously, cover_url was always an empty string. Now it holds the actual cover image URL from the API.
- Save app.py.
- Start the Flask development server by running this in your terminal:
python app.py
- Open http://127.0.0.1:5000/add in your browser.
- Fill in the form with a test book. Enter Dune as the title, Frank Herbert as the author, leave ISBN blank, and enter Bedroom Shelf for location.
- Click Add Book.
You should be redirected to the library page and see your book in the plain text list. The API was called behind the scenes and a cover URL was stored in the database.
Seeing an error about requests?
If you see ModuleNotFoundError: No module named 'requests', make sure your virtual environment is activated. Run source venv/bin/activate and then pip install requests again.
help me fix a ModuleNotFoundError for requests in my Flask app
- Stop the server by pressing Ctrl+C in your terminal.
Redesign the library page as a visual grid
The book is stored with its cover URL, but the template still shows a plain bullet list. Time to transform it into a responsive grid of cards that displays cover images, titles, and locations.
- Open templates/index.html in VS Code.
- Delete all of the existing content. Replace it with this head section containing the page styles:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Book Library</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f5f5f5; padding: 2rem; }
header { text-align: center; margin-bottom: 2rem; }
header h1 { font-size: 2rem; color: #333; margin-bottom: 0.5rem; }
.add-link { display: inline-block; background-color: #4a90d9; color: white; padding: 0.6rem 1.2rem; border-radius: 6px; text-decoration: none; font-size: 0.9rem; }
.add-link:hover { background-color: #357abd; }
.empty-state { text-align: center; color: #888; font-size: 1.1rem; margin-top: 4rem; }
.book-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1.5rem; max-width: 1200px; margin: 0 auto; }
.book-card { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s; }
.book-card:hover { transform: translateY(-4px); }
.book-card img { width: 100%; height: 250px; object-fit: cover; }
.no-cover { width: 100%; height: 250px; background-color: #ddd; display: flex; align-items: center; justify-content: center; color: #888; font-size: 0.9rem; }
.book-info { padding: 0.8rem; }
.book-info h3 { font-size: 0.95rem; color: #333; margin-bottom: 0.2rem; }
.book-info .author { font-size: 0.8rem; color: #666; margin-bottom: 0.4rem; }
.book-info .location { font-size: 0.75rem; color: #4a90d9; font-weight: 500; }
</style>
</head>
- Now add the body section directly below the </head> tag:
<body>
<header>
<h1>My Book Library</h1>
<a href="{{ url_for('add_book') }}" class="add-link">+ Add Book</a>
</header>
{% if books %}
<div class="book-grid">
{% for book in books %}
<div class="book-card">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" alt="{{ book.title }} cover">
{% else %}
<div class="no-cover">No Cover</div>
{% endif %}
<div class="book-info">
<h3>{{ book.title }}</h3>
<p class="author">{{ book.author }}</p>
<p class="location">{{ book.location }}</p>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty-state">No books yet. Add your first book!</p>
{% endif %}
</body>
</html>
What does this template do?
The .book-grid class creates a responsive CSS grid that automatically fills rows with cards at least 180px wide. Each .book-card checks whether a cover_url exists. If it does, it renders the cover as an image. If not, it shows a grey "No Cover" placeholder.
Below the cover, each card displays the book's title, author, and physical location. The grid adapts to any screen width, from a phone to a wide desktop monitor.
- Save templates/index.html.
- Now open templates/add.html to give the form page matching styles. Add this <style> block inside the <head>, directly above the closing </head> tag:
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f5f5f5; padding: 2rem; display: flex; justify-content: center; }
.form-container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); width: 100%; max-width: 500px; }
.form-container h1 { font-size: 1.5rem; color: #333; margin-bottom: 1.5rem; }
label { display: block; font-size: 0.9rem; color: #555; margin-bottom: 0.3rem; font-weight: 500; }
input[type="text"] { width: 100%; padding: 0.6rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; margin-bottom: 1rem; }
input[type="text"]:focus { outline: none; border-color: #4a90d9; }
.hint { font-size: 0.75rem; color: #888; margin-top: -0.8rem; margin-bottom: 1rem; }
button { width: 100%; padding: 0.7rem; background-color: #4a90d9; color: white; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
button:hover { background-color: #357abd; }
.back-link { display: block; text-align: center; margin-top: 1rem; color: #4a90d9; text-decoration: none; font-size: 0.9rem; }
</style>
- Still in templates/add.html, find the back link at the bottom:
<a href="{{ url_for('index') }}">Back to Library</a>
- Add class="back-link" to the tag so it picks up the new styles:
<a href="{{ url_for('index') }}" class="back-link">Back to Library</a>
- Save templates/add.html.
Before you run the app, think about what you expect to see. You added a book called "Dune" earlier without an ISBN. Do you think it will show a cover image, or the grey "No Cover" placeholder?
- Start the server again by running:
python app.py
- Open http://127.0.0.1:5000 in your browser.
You should see the new grid layout. Your "Dune" entry may show a cover (the API found it by title and author) or a grey placeholder. Either is correct. The visual grid itself is the transformation.
Now test with a real ISBN to confirm the full flow works. What do you expect to see when you provide a known ISBN?
- Click the + Add Book button.
- Enter The Hitchhiker's Guide to the Galaxy as the title.
- Enter Douglas Adams as the author.
- Enter 978-0345376596 as the ISBN.
- Enter Living Room, Top Shelf for the location.
- Click Add Book.
The page reloads and you see the book's actual cover image displayed in a card, with "Living Room, Top Shelf" shown beneath it in blue text. Your library now looks like a real bookshelf.
Seeing a grey "No Cover" placeholder instead?
The BookCover API may occasionally be slow or temporarily unavailable. Check that you typed the ISBN correctly, including the dashes. Also confirm your machine has an active internet connection.
If the cover still does not appear, try a different ISBN. The API returns a 404 for books it cannot find.
help me troubleshoot why the BookCover API is not returning a cover image for my book
✔️ Awesome, I've got everything!
Great. Double check you have saved both app.py, templates/index.html, and templates/add.html.
ⓧ I'd like to double check the full code
Here is the complete app.py at this point. Scroll up through your file and confirm each section is present in order: the imports, get_db, init_db, fetch_cover, the index route, the add_book route, and the if __name__ block.
import sqlite3
import requests
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
DATABASE = "books.db"
def get_db():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
conn.execute(
"""
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT NOT NULL,
isbn TEXT,
location TEXT NOT NULL,
cover_url TEXT
)
"""
)
conn.commit()
conn.close()
def fetch_cover(title, author, isbn):
try:
if isbn:
clean_isbn = isbn.replace("-", "")
response = requests.get(
"https://bookcover.longitood.com/bookcover",
params={"isbn": clean_isbn},
timeout=10,
)
else:
response = requests.get(
"https://bookcover.longitood.com/bookcover",
params={"book_title": title, "author_name": author},
timeout=10,
)
if response.status_code == 200:
data = response.json()
return data.get("url", "")
except (requests.RequestException, ValueError):
pass
return ""
@app.route("/")
def index():
conn = get_db()
books = conn.execute("SELECT * FROM books ORDER BY id DESC").fetchall()
conn.close()
return render_template("index.html", books=books)
@app.route("/add", methods=["GET", "POST"])
def add_book():
if request.method == "POST":
title = request.form["title"]
author = request.form["author"]
isbn = request.form.get("isbn", "")
location = request.form["location"]
cover_url = fetch_cover(title, author, isbn)
conn = get_db()
conn.execute(
"INSERT INTO books (title, author, isbn, location, cover_url) VALUES (?, ?, ?, ?, ?)",
(title, author, isbn, location, cover_url),
)
conn.commit()
conn.close()
return redirect(url_for("index"))
return render_template("add.html")
if __name__ == "__main__":
init_db()
app.run(debug=True)
Here is the complete templates/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Book Library</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f5f5f5; padding: 2rem; }
header { text-align: center; margin-bottom: 2rem; }
header h1 { font-size: 2rem; color: #333; margin-bottom: 0.5rem; }
.add-link { display: inline-block; background-color: #4a90d9; color: white; padding: 0.6rem 1.2rem; border-radius: 6px; text-decoration: none; font-size: 0.9rem; }
.add-link:hover { background-color: #357abd; }
.empty-state { text-align: center; color: #888; font-size: 1.1rem; margin-top: 4rem; }
.book-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1.5rem; max-width: 1200px; margin: 0 auto; }
.book-card { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s; }
.book-card:hover { transform: translateY(-4px); }
.book-card img { width: 100%; height: 250px; object-fit: cover; }
.no-cover { width: 100%; height: 250px; background-color: #ddd; display: flex; align-items: center; justify-content: center; color: #888; font-size: 0.9rem; }
.book-info { padding: 0.8rem; }
.book-info h3 { font-size: 0.95rem; color: #333; margin-bottom: 0.2rem; }
.book-info .author { font-size: 0.8rem; color: #666; margin-bottom: 0.4rem; }
.book-info .location { font-size: 0.75rem; color: #4a90d9; font-weight: 500; }
</style>
</head>
<body>
<header>
<h1>My Book Library</h1>
<a href="{{ url_for('add_book') }}" class="add-link">+ Add Book</a>
</header>
{% if books %}
<div class="book-grid">
{% for book in books %}
<div class="book-card">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" alt="{{ book.title }} cover">
{% else %}
<div class="no-cover">No Cover</div>
{% endif %}
<div class="book-info">
<h3>{{ book.title }}</h3>
<p class="author">{{ book.author }}</p>
<p class="location">{{ book.location }}</p>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty-state">No books yet. Add your first book!</p>
{% endif %}
</body>
</html>
Here is the complete templates/add.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Book - My Book Library</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f5f5f5; padding: 2rem; display: flex; justify-content: center; }
.form-container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); width: 100%; max-width: 500px; }
.form-container h1 { font-size: 1.5rem; color: #333; margin-bottom: 1.5rem; }
label { display: block; font-size: 0.9rem; color: #555; margin-bottom: 0.3rem; font-weight: 500; }
input[type="text"] { width: 100%; padding: 0.6rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; margin-bottom: 1rem; }
input[type="text"]:focus { outline: none; border-color: #4a90d9; }
.hint { font-size: 0.75rem; color: #888; margin-top: -0.8rem; margin-bottom: 1rem; }
button { width: 100%; padding: 0.7rem; background-color: #4a90d9; color: white; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
button:hover { background-color: #357abd; }
.back-link { display: block; text-align: center; margin-top: 1rem; color: #4a90d9; text-decoration: none; font-size: 0.9rem; }
</style>
</head>
<body>
<div class="form-container">
<h1>Add a Book</h1>
<form method="POST" action="{{ url_for('add_book') }}">
<label for="title">Title</label>
<input type="text" id="title" name="title" required placeholder="e.g. The Hitchhiker's Guide to the Galaxy">
<label for="author">Author</label>
<input type="text" id="author" name="author" required placeholder="e.g. Douglas Adams">
<label for="isbn">ISBN (optional)</label>
<input type="text" id="isbn" name="isbn" placeholder="e.g. 978-0345376596">
<p class="hint">ISBN-13 helps find the cover image. Leave blank to search by title and author.</p>
<label for="location">Location</label>
<input type="text" id="location" name="location" required placeholder="e.g. Living Room, Top Shelf">
<button type="submit">Add Book</button>
</form>
<a href="{{ url_for('index') }}" class="back-link">Back to Library</a>
</div>
</body>
</html>
Your library has gone from a plain text list to a visual grid of book covers fetched automatically from the internet. Next up, you'll make this accessible from any device on your home network.
Make the Library Accessible on Your LAN
Your book library works perfectly on your own machine, but it can only be reached at http://127.0.0.1:5000. That address is a loopback. It means "this computer only."
The whole point of a home library app is checking it from your phone while standing at a bookshelf. In this step, you will bind Flask to all network interfaces so any device on your home network can pull up the library.
In this step, get ready to:
- Update app.py to listen on all network interfaces instead of just localhost.
- Confirm access from another device on your local network.
Update app.py to bind to all interfaces
Right now your app has no idea what its own network address is. You will add a helper function that detects your machine's LAN IP address, then update the startup block so Flask listens on every interface.
- In app.py, find the import lines at the very top of the file:
import sqlite3
import requests
- Add import socket between them so the top of your file looks like this:
import sqlite3
import socket
import requests
- Scroll down to the bottom of app.py. Find the line that says if __name__ == "__main__":.
- Add the get_local_ip function directly above that line by pasting this code:
def get_local_ip():
# Detect this machine's LAN IP using a UDP socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
# Fall back to localhost if networking is unavailable
return "127.0.0.1"
What does this code do?
This function opens a UDP socket and "connects" to Google's DNS server (8.8.8.8). It never actually sends data. The OS picks the correct local network interface to route that connection, and getsockname() returns the IP address of that interface.
If networking is unavailable, the function falls back to 127.0.0.1 so the app still starts.
- Now find the existing startup block directly below where you just pasted:
if __name__ == "__main__":
init_db()
app.run(debug=True)
- Replace that entire block with the following code:
if __name__ == "__main__":
init_db()
local_ip = get_local_ip()
# Print startup message with both access URLs
print(f"\n Book Library is running!")
print(f" Local: http://127.0.0.1:5000")
print(f" Network: http://{local_ip}:5000\n")
app.run(host="0.0.0.0", port=5000, debug=True)
What does this code do?
- get_local_ip() detects your machine's LAN address and stores it in local_ip.
- The print statements display both the localhost and network URLs so you know exactly where to access the app.
- host="0.0.0.0" tells Flask to listen on every network interface, not just the loopback. This is what makes the app reachable from other devices.
- Save app.py.
- If your Flask server is still running from before, press Ctrl+C in the terminal to stop it.
- Start the app again by running:
python app.py
Before you look at the output, what do you expect to see printed in the terminal? Think about the two URLs the startup block prints.
You should see output like this:
Book Library is running!
Local: http://127.0.0.1:5000
Network: http://192.168.1.42:5000
The Network URL shows your machine's LAN IP. The exact numbers will differ depending on your home network.
Why python app.py instead of flask run?
Running python app.py executes the __main__ block directly, which prints the startup message and passes host="0.0.0.0" to Flask. The flask run command skips the __main__ block entirely, so you would need to pass --host=0.0.0.0 separately.
Seeing a syntax error or NameError?
- Make sure the get_local_ip function sits above the if __name__ block, not inside it. Python needs the function defined before it is called.
- Check that import socket is at the top of the file. Without it, socket.socket throws a NameError.
Help me debug my app.py startup error
Confirm access from another device on your network
The app claims it is reachable on the network. Time to prove it by opening the library on a different device.
- Confirm your LAN IP by opening a second terminal and running:
hostname -I
This prints your machine's IP addresses. The first address should match the Network URL that app.py printed on startup.
- Grab your phone, tablet, or another computer that is connected to the same Wi-Fi or wired network.
- Open a browser on that device and navigate to http:// followed by the IP address from the Network URL, followed by :5000. For example: http://192.168.1.42:5000.
Do you think you will see your books with cover images, or an error? Make a prediction before loading the page.
You should see your full book library with cover images, just as it appears on your computer.
Can't reach the page from another device?
- Make sure both devices are on the same network. A phone on mobile data cannot reach your LAN.
- Check that the URL starts with http:// (not https://). Flask's dev server does not use HTTPS.
- On Fedora, the firewall may block port 5000. Open it temporarily by running sudo firewall-cmd --add-port=5000/tcp in a new terminal, then try again on the other device.
Help me troubleshoot LAN access
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and that the Network URL loads on another device.
ⓧ I'd like to double check the full code
Scroll up and confirm these pieces are in your app.py, in order:
- The three imports at the top: sqlite3, socket, requests.
- The get_local_ip() function placed above the if __name__ block.
- The updated __main__ block calling init_db(), get_local_ip(), printing both URLs, and running with host="0.0.0.0".
Your book library is now a proper home network app. Anyone on your Wi-Fi can browse your collection, check where a book lives, and see its cover. Next up, you can extend the library with search and filter in the Secret Mission.
Secret mission
Add Search and Filter
Your library works, but once it grows past a dozen books, scrolling through the grid to find one becomes tedious. In this mission, you'll add a search bar that instantly filters books by title, author, or location as you type.
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:
- A books.db SQLite database file stored locally on your machine. This is free and takes negligible disk space.
- A Flask development server running on your machine. It only runs while the terminal is open.
- A project folder containing your virtual environment, Python files, and HTML templates.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep accessing your library from other devices on your network.
- Leave the terminal open with the Flask server running.
- Your library remains accessible at http://your-ip:5000 from any device on your local network.
- Your books are stored in books.db and will persist as long as the project folder exists.
Pause - I'll come back to this later
Shut down the running server to free up your terminal. Your files and database stay intact so you can pick up where you left off.
- Press Ctrl+C in the terminal where Flask is running to stop the server.
- Your books are safe in books.db and will reappear when you restart.
- To restart later, activate your virtual environment and run python app.py from your project folder.
Delete - I don't want to use this again
Remove all project files, the virtual environment, and your book database. This cannot be undone.
- Press Ctrl+C in the terminal to stop the Flask server if it is still running.
- Navigate to the parent directory (the Desktop or wherever you created the project folder) by running:
cd ~/Desktop
- Delete the entire project folder by running:
rm -rf book-library
- Confirm the folder is gone by running:
ls
- You should no longer see your project folder in the output. Everything is cleaned up.
That's a Wrap!
That's a Wrap!
Nice work. You just built a full-stack web application from scratch that fetches real book covers, stores your collection in a database, and serves it to any device on your home network.
You've learned how to:
- Build a Flask web application from scratch with routes, HTML templates, and a SQLite database you designed.
- Connect to an external REST API (BookCover API) to fetch book cover images automatically based on ISBN or title.
- Serve the application over your local network so any device in your house can access your library.
- Secret Mission: Added real-time client-side search filtering with JavaScript.
Ready to quiz yourself?