Build a LAN Inventory Scanner
Build a full-stack app to photograph and catalog items on your home network.
Introduction
30 Second Summary
Everyone owns more things than they can keep track of. Whether it's tools in your garage, books on your shelves, or cables in a drawer, knowing what you have and where you put it saves real time and frustration.
In this project, you will build a local network inventory scanner using Express and SQLite that runs on your computer and is accessible from any device in your home. You'll snap photos of items from your phone, tag them with a name and location, and browse everything in a visual grid.
What You'll Build
You photograph a book from your phone, submit the form, and within seconds it appears in a visual inventory grid that any device on your home network can see.
By the end of this project, you'll have:
- An inventory app accessible from any device on your local network where you can add items with a name, location, and notes.
- Phone-to-server photo uploads that snap a picture with your rear camera and send it to the server in one tap.
- A visual grid displaying all your items with thumbnail images, backed by a single SQLite file that persists across restarts.
- Secret Mission: Add real-time search and filtering so you can find items instantly as your inventory grows.
Are there any prerequisites?
No prior experience is needed. This guide walks you through every tool installation and setup step from scratch.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to you.
Set Up the Project
Every web app starts with a project folder and its dependencies installed. Before you can build any inventory features, you need Express (the web server), better-sqlite3 (the database), and Multer (for file uploads) ready to go.
In this step, you'll scaffold the project, write a minimal server, and prove it's reachable from your phone over your local network.
In this step, get ready to:
- Create a Node.js project and install Express, better-sqlite3, and Multer.
- Write a minimal Express server and run it.
- Confirm the server is accessible from your phone over LAN.
Create a project folder and initialize it
You need a dedicated folder on your Desktop for this project so you can find it easily later.
- Press the Super key to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir inventory-scanner
cd inventory-scanner
- Initialize a new Node.js project by running:
npm init -y
You should see JSON output showing your new package.json with fields like name and version.
- Install the three packages your project needs by running:
npm install express@5.2.1 better-sqlite3@13.0.1 multer@2.2.0
What do these packages do?
- express@5.2.1 is the web framework that handles HTTP requests and routes.
- better-sqlite3@13.0.1 provides a synchronous SQLite database. Perfect for a single-user local app because you get simple code without callbacks.
- multer@2.2.0 handles file uploads. It saves incoming photos to disk so you can serve them back later.
You should see output ending with something like "added X packages" confirming the install succeeded.
Seeing network errors?
Make sure you have an internet connection. If you're behind a corporate proxy, you may need to configure npm's proxy settings.
If you see permission errors, do not use sudo. Instead, check that your user owns the project folder.
Help me fix my npm install error
- Create the uploads/ directory for storing images by running:
mkdir uploads
- Confirm everything is in place by running:
ls
You should see node_modules, package.json, package-lock.json, and uploads listed in the output.
- Open this folder in VS Code by running:
code .
What does code . do?
The code command launches VS Code. The . tells it to open the current folder as a workspace, so the file explorer shows your entire project.
Create a minimal server that proves Express is working
Before building features, you want the shortest possible proof that Express is alive and responding to requests.
- In VS Code, create a new file called server.js by clicking File in the menu bar, then New File.
- Save it immediately with Ctrl+S and name it server.js in the inventory-scanner folder.
- Paste the following code into server.js:
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send('Hello from Inventory Scanner');
});
const server = app.listen(PORT, '0.0.0.0', (error) => {
if (error) {
throw error;
}
console.log(`Server running at http://0.0.0.0:${PORT}`);
});
What does this code do?
- require('express') imports the Express framework you just installed.
- app.get('/', ...) defines a route. When someone visits the root URL, the server sends back a plain text greeting.
- app.listen(PORT, '0.0.0.0', ...) starts the server on all network interfaces. Binding to 0.0.0.0 means other devices on your LAN (like your phone) can reach it, not just localhost.
- The callback checks for an error as its first argument. This is how Express 5 reports startup failures (different from Express 4 tutorials you might find online).
- Save server.js with Ctrl+S.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, top-left of your keyboard).
Before you run the next command, what do you think will appear in the terminal?
- Start the server by running:
node server.js
You should see: Server running at http://0.0.0.0:3000.
- Open your browser and navigate to http://localhost:3000.
You should see "Hello from Inventory Scanner" displayed on the page.
Seeing "address already in use"?
Another process is using port 3000. Find it with lsof -i :3000 and stop it, or change the PORT value in server.js to another number like 3001.
Help me fix my Express server startup error
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js and leave the server running in your terminal.
ⓧ I'd like to double check the full code
Here is the complete server.js file. Make sure yours matches exactly:
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send('Hello from Inventory Scanner');
});
const server = app.listen(PORT, '0.0.0.0', (error) => {
if (error) {
throw error;
}
console.log(`Server running at http://0.0.0.0:${PORT}`);
});
Find your machine's LAN IP and test from your phone
Your server is running, but right now only your computer can reach it via localhost. To access it from your phone, you need your machine's local network IP address.
- Open a second terminal in VS Code by pressing Ctrl+Shift+` (this keeps the server running in the first terminal).
- Find your LAN IP address by running:
hostname -I
You'll see one or more IP addresses printed. Look for the one matching your home network (typically starts with 192.168. or 10.).
Which IP do I use?
The hostname -I command may print multiple addresses if your machine has several network interfaces (Wi-Fi, Ethernet, virtual). Pick the one on the same subnet as your phone. If your phone is on Wi-Fi at 192.168.1.x, use the address that also starts with 192.168.1.
- On your phone, make sure it's connected to the same Wi-Fi network as your computer.
- Open your phone's browser and type http:// followed by your LAN IP and :3000 (for example, http://192.168.1.42:3000).
You should see the same "Hello from Inventory Scanner" message on your phone.
Phone can't reach the server?
Fedora's firewall blocks incoming connections by default. Open port 3000 temporarily by running this in your second terminal:
sudo firewall-cmd --add-port=3000/tcp
This lasts until your next reboot. After running it, try your phone again.
Also confirm your phone and computer are on the same Wi-Fi network. A phone on mobile data cannot reach your LAN.
Help me connect my phone to my Express server
Your project foundation is solid. You have a running Express server that any device on your network can reach. Next up, you'll add a SQLite database so items you add actually persist between restarts.
Build the Inventory List with SQLite
Your server responds to requests, but it has no memory. Every item you add would vanish the moment you restart the process.
In this step, you will connect a SQLite database to your Express server and build a form that saves inventory items into it. By the end, you will have a working add-and-list flow where data survives a full server restart.
In this step, get ready to:
- Set up a SQLite database and create an items table.
- Build an HTML page with a form and an item listing.
- Wire the frontend to fetch items on load and submit new ones.
Set up the SQLite database and create the items table
better-sqlite3 is a synchronous SQLite library. Synchronous means your code waits for each database call to finish before moving on. For a single-user local app, that keeps the code simple with no callbacks or promises needed for queries.
- If your server is still running from the previous step, stop it by pressing Ctrl+C in the terminal.
- Open server.js in VS Code and replace its entire contents with the following database setup code:
const express = require('express');
const Database = require('better-sqlite3');
const app = express();
const PORT = 3000;
// Database setup
const db = new Database('inventory.db');
db.exec(`
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
location TEXT,
notes TEXT,
image_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
What does this code do?
- new Database('inventory.db') creates (or opens, if it already exists) a SQLite database file in your project folder.
- db.exec() runs the SQL to create the items table. IF NOT EXISTS means it skips table creation if the table is already there from a previous run.
- express.json() and express.urlencoded() let Express parse incoming request bodies so your routes can read form data.
- express.static('public') serves any file inside a public/ folder as a static asset (HTML, CSS, images).
- Now add the API routes and server listener below the middleware section in the same file:
// API routes
app.get('/api/items', (req, res) => {
const items = db.prepare('SELECT * FROM items ORDER BY created_at DESC').all();
res.json(items);
});
app.post('/api/items', (req, res) => {
const { name, location, notes } = req.body;
const stmt = db.prepare(
'INSERT INTO items (name, location, notes) VALUES (?, ?, ?)'
);
const info = stmt.run(name, location, notes);
res.json({ id: info.lastInsertRowid, name, location, notes });
});
// Start server on all interfaces
const server = app.listen(PORT, '0.0.0.0', (error) => {
if (error) {
throw error;
}
console.log(`Server running at http://0.0.0.0:${PORT}`);
console.log(`Access from other devices: http://<your-local-ip>:${PORT}`);
});
What do the API routes do?
- GET /api/items runs a prepared statement that selects every row from the items table and returns them as a JSON array.
- POST /api/items pulls name, location, and notes from the request body, inserts them with a prepared statement (the ? placeholders prevent SQL injection), and returns the new item with its ID.
- info.lastInsertRowid gives you the auto-generated id of the row that was just inserted.
- Save server.js.
- Start the server by running:
node server.js
You should see both log lines printed, including the new "Access from other devices" message.
- Open a second terminal and test the GET route by running:
curl http://localhost:3000/api/items
You should see an empty JSON array: []. That means the database connected and the table was created successfully.
Seeing an error about better-sqlite3?
Make sure you are in the inventory-scanner folder when you run node server.js. The node_modules folder must be in the same directory.
If you see a compilation error, try deleting node_modules and running npm install again. help me fix a better-sqlite3 import error
Create an HTML page with a form and item listing
The API routes work, but there is no user interface yet. You need an HTML page with a form so you can add items from a browser instead of curl.
- In your second terminal (leave the server running in the first), create the public directory by running:
mkdir public
- Confirm the folder exists by running:
ls
You should see public listed alongside node_modules, server.js, uploads, and inventory.db (the database file that appeared when you started the server).
- In VS Code, create a new file inside public/ called index.html and paste the following structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventory Scanner</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
h1 { text-align: center; margin-bottom: 20px; color: #333; }
.add-form { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.add-form input, .add-form textarea { width: 100%; padding: 10px; margin-bottom: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; }
.add-form button { width: 100%; padding: 12px; background: #2563eb; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; }
.add-form button:hover { background: #1d4ed8; }
.item-list { list-style: none; }
.item-list li { background: white; padding: 12px; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
</style>
</head>
<body>
<h1>Inventory Scanner</h1>
<form class="add-form" id="addForm">
<input type="text" name="name" placeholder="Item name" required>
<input type="text" name="location" placeholder="Location (e.g., Living Room Shelf)">
<textarea name="notes" placeholder="Notes (optional)" rows="2"></textarea>
<button type="submit">Add Item</button>
</form>
<ul class="item-list" id="itemList"></ul>
<script>
</script>
</body>
</html>
What does this page include?
- A form with fields for name, location, and notes. The name field is marked required so the browser blocks empty submissions.
- An empty <ul> with id itemList that JavaScript will fill with inventory items.
- Inline CSS that gives the form a card-like look and styles list items as white rounded boxes on a light background.
- Save public/index.html.
- Open http://localhost:3000 in your browser.
You should see the "Inventory Scanner" heading and a clean white form with three input fields and a blue "Add Item" button. The form does not do anything yet because the <script> tag is empty.
Still seeing "Hello from Inventory Scanner" text?
The old GET / route no longer exists in the new server.js. If you still see it, your server is running the old version. Press Ctrl+C to stop it, then run node server.js again to pick up your changes.
help me troubleshoot why my HTML page is not loading
Wire the frontend to fetch and display items
The page loads, but the form has no behavior. You need JavaScript that fetches existing items on load and sends new items to the server when the form is submitted.
- In public/index.html, find the empty <script> tag and paste the following code between <script> and </script>:
async function loadItems() {
const response = await fetch('/api/items');
const items = await response.json();
const list = document.getElementById('itemList');
list.innerHTML = items.map(item => `<li><strong>${item.name}</strong> — ${item.location || 'No location'}${item.notes ? ' — ' + item.notes : ''}</li>`).join('');
}
document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const body = Object.fromEntries(formData);
await fetch('/api/items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
e.target.reset();
loadItems();
});
loadItems();
What does this script do?
- loadItems() fetches all items from your API and renders them as list items inside the <ul>. It runs once on page load so existing items appear immediately.
- The submit listener intercepts the form, converts the fields to JSON, POSTs them to /api/items, resets the form, and reloads the list.
- Object.fromEntries(formData) converts the form fields into a plain object like { name: "Book", location: "Shelf" } so it can be sent as JSON.
- Save public/index.html.
- Refresh http://localhost:3000 in your browser.
- Type Coffee Mug in the name field, Kitchen Counter in location, and click Add Item.
You should see "Coffee Mug" appear below the form as a white card showing the name and location.
Before you test persistence, what do you think happens if you stop the server and start it again? Will the item survive?
- Go back to the terminal running the server and press Ctrl+C to stop it.
- Restart it by running:
node server.js
- Refresh your browser.
"Coffee Mug" is still there. The data lives in inventory.db on disk, so it persists across restarts.
Item disappeared after restart?
Check that your POST route is inserting into the database and not just returning a hard-coded response. Open server.js and confirm the stmt.run(name, location, notes) line is present inside the POST handler.
help me debug why my inventory items are not persisting after server restart
✔️ Awesome, I've got everything!
Great. Confirm you have saved both server.js and public/index.html.
ⓧ I'd like to double check the full code
Your server.js should contain these sections in order (scroll up to confirm each one):
- Imports (express and better-sqlite3).
- Database setup with new Database('inventory.db') and the CREATE TABLE IF NOT EXISTS statement.
- Middleware: express.json(), express.urlencoded(), express.static('public').
- GET /api/items route returning all items as JSON.
- POST /api/items route inserting a new item with a prepared statement.
- app.listen on port 3000 bound to 0.0.0.0 with the error-first callback.
Your public/index.html should contain these sections in order:
- Head with inline CSS styles for .add-form, .item-list, and base reset.
- Form with id addForm containing name, location, and notes inputs.
- Empty <ul> with id itemList.
- Script with loadItems() function, form submit listener, and the initial loadItems() call.
Your inventory list works. Items persist in the database and show up as text. Next up, you will add photo uploads from your phone so each item has a picture that makes it instantly recognizable.
Add Photo Uploads from Your Phone
Your inventory list works, but every item looks the same. A wall of text makes it impossible to quickly identify items at a glance.
Adding photos transforms the app from a boring list into a visual catalog you actually want to use. In this step, you will configure Multer to handle file uploads, wire up your phone's camera, and display items as a responsive image grid.
In this step, get ready to:
- Configure Multer for disk storage with unique filenames.
- Update the HTML form to support camera capture from your phone.
- Transform the item list into a visual grid with thumbnails.
Configure Multer for disk storage with unique filenames
Multer is Express middleware that handles multipart/form-data requests, which is how browsers send file uploads. You need to tell Multer where to save files and how to name them so that no two uploads overwrite each other.
- In server.js, find the line that says const Database = require('better-sqlite3'); and add the following two imports directly below it:
const multer = require('multer');
const path = require('path');
- Now find the closing }); of the db.exec() block (after the CREATE TABLE statement). Add the Multer storage configuration directly below it:
// Multer setup for file uploads
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
const ext = path.extname(file.originalname);
cb(null, Date.now() + ext);
}
});
const upload = multer({ storage: storage });
What does this code do?
- destination tells Multer to save uploaded files into the uploads/ folder you created earlier.
- filename generates a unique name using the current timestamp plus the original file extension. This prevents two uploads from colliding even if they share the same original name.
- const upload = multer({ storage: storage }) creates a middleware instance you can attach to specific routes.
- Find the line app.use(express.static('public')); in the Middleware section. Add this line directly below it to serve uploaded images:
app.use('/uploads', express.static('uploads'));
- Now find the existing POST route. Replace the entire app.post('/api/items', ...) block with this updated version that accepts file uploads:
app.post('/api/items', upload.single('photo'), (req, res) => {
const { name, location, notes } = req.body;
const image_path = req.file ? req.file.filename : null;
const stmt = db.prepare(
'INSERT INTO items (name, location, notes, image_path) VALUES (?, ?, ?, ?)'
);
const info = stmt.run(name, location, notes, image_path);
res.json({ id: info.lastInsertRowid, name, location, notes, image_path });
});
What changed in the POST route?
- upload.single('photo') is middleware that extracts a single file from the field named photo and attaches it to req.file.
- req.file ? req.file.filename : null safely handles submissions where the user skips the photo.
- The INSERT now saves the generated filename into the image_path column so you can serve the image later.
- Save server.js.
- Switch to the terminal where your server is running and stop it with Ctrl+C.
- Restart the server by running:
node server.js
- You should see Server running at http://0.0.0.0:3000 printed again, confirming Multer loaded without errors.
Server won't start?
- Check for a SyntaxError in the terminal output. The most common cause is a missing comma or closing bracket in the Multer config object.
- Make sure const multer = require('multer'); appears after the better-sqlite3 require, not inside a function.
- Confirm the uploads/ directory exists at your project root. If not, run mkdir uploads and try again.
Why won't my server start after adding Multer?
Update the HTML form to support camera capture
The form currently sends data as JSON, which cannot carry binary file data. You need to switch to multipart/form-data encoding and add a file input that triggers the phone camera.
- In public/index.html, find this line:
<form class="add-form" id="addForm">
- Replace it with this version that includes the enctype attribute:
<form class="add-form" id="addForm" enctype="multipart/form-data">
- Find the <button type="submit">Add Item</button> line. Add this file input directly above it:
<input type="file" name="photo" accept="image/*" capture="environment">
What does capture="environment" do?
On Android and iOS, capture="environment" tells the browser to open the rear-facing camera directly. This is ideal for photographing objects around your house.
On your Fedora desktop browser, this attribute is ignored and you get a normal file picker instead. That is expected behavior.
- Now scroll down to the <script> section. Find the existing submit event listener:
document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const body = Object.fromEntries(formData);
await fetch('/api/items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
e.target.reset();
loadItems();
});
- Replace it with this version that sends the FormData directly (which carries file data):
document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
await fetch('/api/items', {
method: 'POST',
body: formData
});
e.target.reset();
loadItems();
});
Why send FormData instead of JSON?
JSON can only carry text. When you send a FormData object directly, the browser automatically sets the content type to multipart/form-data and packages the file's binary data alongside the text fields. That is how Multer receives it on the server side.
- Save public/index.html.
- Open http://localhost:3000 in your browser. You should see a new file input (a Choose File button) appear in the form.
- Select any image from your computer, fill in a name, and click Add Item.
- Confirm the upload landed on disk by running this in a new terminal:
ls uploads/
- You should see a file named with a timestamp and image extension (e.g., 1718000000000.jpg).
uploads/ folder is empty after submitting?
- Make sure your form tag includes enctype="multipart/form-data". Without it, the browser sends the form as URL-encoded text and Multer never receives the file.
- Check that the file input's name attribute is photo. This must match the string in upload.single('photo') on the server.
- Confirm you restarted the server after editing server.js. Changes to server code are not picked up automatically.
Why aren't my uploaded photos appearing in the uploads folder?
Transform the item list into a visual grid with thumbnails
Photos are uploading successfully, but the page still displays items as plain text. Time to replace the list with a responsive card grid that shows each item's thumbnail.
- In public/index.html, find these two CSS rules inside the <style> block:
.item-list { list-style: none; }
.item-list li { background: white; padding: 12px; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
- Delete those two rules and replace them with these grid and card styles:
.inventory-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.item-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.item-card img {
width: 100%;
height: 200px;
object-fit: cover;
}
.item-card .no-image {
width: 100%;
height: 200px;
background: #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
font-size: 14px;
}
What do these styles do?
- .inventory-grid creates a responsive grid that fills as many 250px-wide columns as will fit.
- .item-card gives each item a white card with rounded corners. overflow: hidden clips the image to the card's rounded shape.
- object-fit: cover ensures photos fill the 200px thumbnail area without stretching.
- .no-image provides a grey placeholder for items that have no photo.
- Still inside the <style> block, add these additional styles directly below the ones you just pasted:
.item-card .info {
padding: 12px;
}
.item-card .info h3 {
margin-bottom: 4px;
color: #333;
}
.item-card .info p {
color: #666;
font-size: 14px;
margin-bottom: 2px;
}
.no-items {
text-align: center;
color: #999;
padding: 40px;
font-size: 18px;
}
- Now find the HTML line:
<ul class="item-list" id="itemList"></ul>
- Replace it with:
<div class="inventory-grid" id="inventoryGrid"></div>
- Finally, replace the entire <script> section (everything between <script> and </script>) with this new rendering logic:
async function loadItems() {
const response = await fetch('/api/items');
const items = await response.json();
renderItems(items);
}
function renderItems(items) {
const grid = document.getElementById('inventoryGrid');
if (items.length === 0) {
grid.innerHTML = '<div class="no-items">No items yet. Add your first item above!</div>';
return;
}
grid.innerHTML = items.map(item => `
<div class="item-card">
${item.image_path
? `<img src="/uploads/${item.image_path}" alt="${item.name}">`
: '<div class="no-image">No photo</div>'
}
<div class="info">
<h3>${item.name}</h3>
<p>${item.location || 'No location'}</p>
${item.notes ? `<p>${item.notes}</p>` : ''}
</div>
</div>
`).join('');
}
document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
await fetch('/api/items', {
method: 'POST',
body: formData
});
e.target.reset();
loadItems();
});
loadItems();
What does renderItems do?
- It takes the array of items from the API and builds an HTML card for each one.
- If an item has an image_path, it renders an <img> tag pointing to /uploads/filename.jpg. Otherwise it shows a grey "No photo" placeholder.
- The empty-state check shows a friendly message when no items exist yet.
- Save public/index.html.
- Refresh http://localhost:3000 in your browser.
You should see the item you uploaded earlier displayed as a card with its photo thumbnail at the top, name, and location underneath. If you added items without photos in the previous step, those appear with a grey "No photo" placeholder.
Cards showing but images are broken?
- Check that app.use('/uploads', express.static('uploads')); is in your server.js. Without this line, the browser cannot reach the image files.
- Open your browser's DevTools Network tab, refresh, and look for a 404 on the image request. The URL should be /uploads/1718000000000.jpg (matching a real file in your uploads/ folder).
- Verify the image_path column has values by checking the previous items in your database. Items added before you configured Multer will have null for image_path and show the placeholder instead.
Why are my inventory card images showing as broken?
Now for the real test. Before you run this, do you think your phone will open the camera automatically when you tap the file input?
- Open your phone's browser and go to http://<your-ip>:3000 (the same LAN address from Step 1).
- Tap the file input. On Android, your rear camera opens directly. On iOS, you may see an option to take a photo or choose from library.
- Snap a photo of any item near you, fill in a name and location, and tap Add Item.
- The photo should appear as a thumbnail card in the grid within a second.
Refresh the page on your desktop browser too. The same item with its photo appears on both devices because they share the same server.
Phone can't reach the server?
Your phone must be on the same Wi-Fi network as your Fedora machine. If you opened port 3000 with firewall-cmd in Step 1, that rule persists until reboot. If you rebooted since then, run sudo firewall-cmd --add-port=3000/tcp again.
✔️ Awesome, I've got everything!
Great. Double check you have saved both server.js and public/index.html.
ⓧ I'd like to double check the full code
Scroll up and confirm each of these code blocks is present in your files, in order:
server.js:
- The multer and path imports (after the better-sqlite3 require).
- The Multer diskStorage config and const upload line (after the database setup).
- app.use('/uploads', express.static('uploads')); in the middleware section.
- The updated POST route with upload.single('photo') and image_path handling.
public/index.html:
- Form tag has enctype="multipart/form-data".
- File input with name="photo" accept="image/*" capture="environment" before the button.
- CSS uses .inventory-grid, .item-card, .no-image, .info, and .no-items classes (no .item-list styles remaining).
- HTML has <div class="inventory-grid" id="inventoryGrid"> instead of <ul>.
- Script has loadItems(), renderItems(), and the FormData submit handler (no JSON.stringify).
Your inventory scanner now has full photo support. You can walk around your house, snap photos from your phone, and see them appear instantly in a visual grid accessible from any device on your network.
Secret mission
Add Search and Filtering
Your inventory grid works, but once you have 20+ items, finding something means scrolling through the entire list. Can you add a search box that filters items in real-time 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:
- inventory-scanner project folder on your Desktop (contains all code, dependencies, database, and uploads).
- inventory.db SQLite database file storing your inventory items.
- uploads/ directory containing your uploaded photos.
Keep everything running
No action needed. Choose this if you want to keep using the inventory scanner on your home network.
- Your data, photos, and code all stay on disk inside the inventory-scanner folder.
- The server stops when you close the terminal or press Ctrl+C. Start it again anytime by running node server.js from inside the project folder.
- There are no ongoing costs. Everything is local files on your machine.
Pause - I'll come back to this later
Stop the running server to free up your terminal. Your data and photos stay safely on disk.
- Press Ctrl+C in the terminal where the server is running to stop it.
- Your inventory.db file and all uploaded photos remain untouched.
- When you want to use the app again, open a terminal, navigate to the project folder, and run node server.js.
Delete - I don't want to use this again
Remove all project files, your database, and all uploaded photos permanently.
- Press Ctrl+C in the terminal where the server is running to stop it.
- Delete the entire project folder by running this command:
rm -rf ~/Desktop/inventory-scanner
- Confirm the folder is gone by running:
ls ~/Desktop/inventory-scanner
You should see a "No such file or directory" message. All your inventory data and photos are permanently deleted.
Nice Work!
Nice Work!
You just built a full-stack inventory scanner from scratch. It runs on your local network, accepts photos from your phone, and persists everything in a database file sitting right next to your code.
You've learned how to:
- Build a full-stack Express 5 web server from scratch, bound to 0.0.0.0 so any device on your LAN can reach it.
- Store persistent data in SQLite using better-sqlite3 with prepared statements for safe inserts and queries.
- Handle file uploads from a phone camera using Multer with DiskStorage, serving images back as static files in a responsive visual grid.
- Secret Mission: Added real-time search filtering across multiple fields so you can find any item instantly as your inventory grows.
Ready to quiz yourself?