Programming lesson
Building an Album Search and Save Tool: Mastering Arrays, Fetch, and DOM APIs
Learn how to build an album search and save tool using arrays, fetch, DOM APIs, and ES modules. This step-by-step tutorial covers tab navigation, REST API integration, and local persistence.
Introduction: Why Build an Album Search Tool?
In today's music-streaming era, apps like Spotify and Apple Music rely on efficient search and save features. This tutorial walks you through building a similar tool using JavaScript arrays, Fetch API, and DOM manipulation—core skills for any web developer. You'll learn to navigate tabs, fetch album data from a REST API, and manage a personal album collection. Whether you're a student tackling CPSC 1520 or a hobbyist, this guide will level up your frontend skills.
Setting Up Your Project
Before coding, set up the project structure. You'll need two terminals: one for the backend server and one for the frontend.
Backend Setup
Extract the provided assignment-3-backend folder. Run npm install to install dependencies. Check the package.json scripts to start the backend—usually npm start. Leave this terminal running.
Frontend Setup
In a separate terminal, initialize your frontend project. Install Parcel and Bootstrap:
npm init -y
npm install parcel bootstrapAdd scripts to package.json:
"scripts": {
"start": "parcel index.html",
"build": "parcel build index.html"
}In your main.js, import Bootstrap and your custom CSS:
import 'bootstrap/dist/css/bootstrap.min.css';
import '../css/cover.css';Run npm start to launch the dev server. Pro tip: Avoid including node_modules or parcel-cache in your final submission.
Part 1: Tab Navigation
Tab navigation is like switching between playlists in a music app. We'll use DOM APIs to toggle visibility.
Get the Elements
Select the tab container and individual tabs:
const albumTabNav = document.getElementById('album-tab-navigation');
const searchTab = document.getElementById('search-album-tab');
const myAlbumsTab = document.getElementById('myalbums-tab');
const searchLink = albumTabNav.querySelector('a[href="#search"]');
const myAlbumsLink = albumTabNav.querySelector('a[href="#myalbums"]');Add Click Event Listener
Listen for clicks on the tab navigation. Use event.target.textContent to determine which tab was clicked.
albumTabNav.addEventListener('click', (event) => {
const tabName = event.target.textContent.trim();
if (tabName === 'Search Albums') {
searchTab.classList.add('active');
myAlbumsTab.classList.remove('active');
document.getElementById('search-album-tab').classList.remove('d-none');
document.getElementById('myalbums-tab').classList.add('d-none');
} else if (tabName === 'My Albums') {
myAlbumsTab.classList.add('active');
searchTab.classList.remove('active');
document.getElementById('myalbums-tab').classList.remove('d-none');
document.getElementById('search-album-tab').classList.add('d-none');
}
});This pattern is similar to how AI chatbots switch between conversation modes. The d-none class from Bootstrap hides the inactive panel.
Part 2: Search Albums with REST API
Now we integrate the backend. We'll create a function in api/album.js to fetch albums by name.
Create the Fetch Function
Use the Fetch API to call the backend endpoint. The backend runs on http://localhost:3000 (check README).
// api/album.js
export async function searchAlbums(query) {
const response = await fetch(`http://localhost:3000/api/albums?q=${encodeURIComponent(query)}`);
if (!response.ok) {
throw new Error('Failed to fetch albums');
}
return response.json();
}Create DOM Elements
In dom/albumElements.js, build a function to render each album as a list item.
// dom/albumElements.js
export function createAlbumListItem(album) {
const li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-center';
li.innerHTML = `
${album.title} by ${album.artist}
Add to My Albums
`;
return li;
}Handle Form Submission
In main.js, listen for the search form submission.
import { searchAlbums } from './api/album.js';
import { createAlbumListItem } from './dom/albumElements.js';
document.getElementById('search-album-form').addEventListener('submit', async (event) => {
event.preventDefault();
const query = document.getElementById('search-input').value;
const albumList = document.getElementById('searched-albums');
albumList.innerHTML = ''; // Clear previous results
try {
const albums = await searchAlbums(query);
albums.forEach(album => {
const li = createAlbumListItem(album);
albumList.appendChild(li);
});
} catch (error) {
console.error('Search failed:', error);
}
});This is similar to how Spotify's search instantly fetches results from their API. Notice we clear the list before adding new results—this resets the state as required.
Part 3: Add to My Albums
Now we enable saving albums to a personal list. When the user clicks "Add to My Albums," the album moves from search results to the "My Albums" tab.
Event Delegation for Buttons
Since the add buttons are dynamically created, use event delegation on the album list container.
document.getElementById('searched-albums').addEventListener('click', (event) => {
if (event.target.classList.contains('add-btn')) {
const li = event.target.closest('li');
const albumData = {
title: li.querySelector('span').textContent.split(' by ')[0],
artist: li.querySelector('span').textContent.split(' by ')[1]
};
addToMyAlbums(albumData);
li.remove(); // Remove from search results
}
});
function addToMyAlbums(album) {
const myAlbumsList = document.getElementById('my-albums-list');
const li = document.createElement('li');
li.className = 'list-group-item';
li.textContent = `${album.title} by ${album.artist}`;
myAlbumsList.appendChild(li);
}Duplicates are allowed, so no duplicate check needed. This mimics how you might save songs to a playlist in Apple Music.
Bonus: Persist Albums with REST API
For extra credit, save albums to the backend so they persist on page reload. You'll need to add a POST endpoint in the backend's api.js.
Backend Endpoint (api.js)
// In assignment-3-backend/api.js
app.post('/api/albums/save', (req, res) => {
const album = req.body;
// Read existing database, append album, write back
const db = JSON.parse(fs.readFileSync('./albums_app_db.json'));
db.myAlbums.push(album);
fs.writeFileSync('./albums_app_db.json', JSON.stringify(db, null, 2));
res.status(201).json({ message: 'Album saved' });
});Frontend Save Function
Modify addToMyAlbums to also POST data.
async function addToMyAlbums(album) {
// POST to backend
await fetch('http://localhost:3000/api/albums/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(album)
});
// Update UI
const myAlbumsList = document.getElementById('my-albums-list');
const li = document.createElement('li');
li.className = 'list-group-item';
li.textContent = `${album.title} by ${album.artist}`;
myAlbumsList.appendChild(li);
}Load Saved Albums on Page Load
When the app starts, fetch saved albums from the backend.
async function loadSavedAlbums() {
const response = await fetch('http://localhost:3000/api/albums/saved');
const albums = await response.json();
const myAlbumsList = document.getElementById('my-albums-list');
albums.forEach(album => {
const li = document.createElement('li');
li.className = 'list-group-item';
li.textContent = `${album.title} by ${album.artist}`;
myAlbumsList.appendChild(li);
});
}
loadSavedAlbums();This bonus part turns your app into a persistent album manager, similar to how Notion saves your database entries.
Common Pitfalls and Tips
- Backend not running: Ensure the backend server is active before testing search. Use two terminals.
- CORS issues: If the backend blocks requests, add
corsmiddleware or use a proxy in Parcel. - Duplicate event listeners: Avoid attaching listeners inside loops. Use event delegation.
- State reset: Always clear the search list before displaying new results.
- Asynchronous errors: Wrap fetch calls in try-catch to handle network failures gracefully.
Conclusion
You've built a fully functional album search and save tool using arrays and loops, fetch fundamentals, DOM APIs and timers, and ES modules. These skills are transferable to building AI-powered recommendation systems, e-commerce product searches, or social media feeds. Keep experimenting—try adding delete functionality or sorting albums by year. Happy coding!