Building projects is the fastest way to master JavaScript. Tutorials teach you syntax — projects give you real debugging experience, problem-solving skills, and something concrete to show employers. This guide walks through 25 JavaScript projects organized by difficulty, with starter code, what you learn, and time estimates.
At a glance
| Level | Projects | Time each | Key skills |
|---|---|---|---|
| Beginner | #1–8 | 2–6 hours | DOM, events, variables, functions |
| Intermediate | #9–16 | 1–3 days | APIs, async/await, OOP, storage |
| Advanced | #17–25 | 1–2 weeks | React, Node.js, real-time, full-stack |
Tip: Even if you have some experience, don't skip the beginner section — the habits you build (event delegation, input validation, clean DOM manipulation) matter more than the complexity of the project.
Beginner projects (#1–8)
#1 — Digital clock
What you build: A live clock that updates every second, displaying hours, minutes, and seconds — with AM/PM support and zero-padding.
What you learn: setInterval, Date object, DOM manipulation, string formatting.
Time: 1–2 hours | Difficulty: ⭐
<!DOCTYPE html>
<html lang="en">
<head>
<title>Digital Clock</title>
<style>
body { display: flex; justify-content: center; align-items: center; height: 100vh; background: #111; }
#clock { font-size: 4rem; font-family: monospace; color: #0f0; letter-spacing: 0.1em; }
</style>
</head>
<body>
<div id="clock"></div>
<script>
function updateClock() {
const now = new Date();
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
document.getElementById('clock').textContent = `${h}:${m}:${s}`;
}
updateClock();
setInterval(updateClock, 1000);
</script>
</body>
</html>
Extend it: Add a date display, theme switcher (dark/light), or alarm functionality.
#2 — To-do list
What you build: A task manager where users can add, complete, and delete tasks — with persistence via localStorage.
What you learn: DOM manipulation, event listeners, localStorage, array methods.
Time: 2–3 hours | Difficulty: ⭐
const form = document.getElementById('todo-form');
const input = document.getElementById('todo-input');
const list = document.getElementById('todo-list');
let tasks = JSON.parse(localStorage.getItem('tasks') || '[]');
function save() {
localStorage.setItem('tasks', JSON.stringify(tasks));
}
function render() {
list.innerHTML = '';
tasks.forEach((task, i) => {
const li = document.createElement('li');
li.textContent = task.text;
if (task.done) li.style.textDecoration = 'line-through';
li.onclick = () => { tasks[i].done = !tasks[i].done; save(); render(); };
const btn = document.createElement('button');
btn.textContent = '✕';
btn.onclick = (e) => { e.stopPropagation(); tasks.splice(i, 1); save(); render(); };
li.appendChild(btn);
list.appendChild(li);
});
}
form.onsubmit = (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
tasks.push({ text, done: false });
input.value = '';
save();
render();
};
render();
Extend it: Add due dates, priority levels, or drag-to-reorder functionality.
#3 — Number guessing game
What you build: The browser picks a random number 1–100; the user guesses, receiving "too high / too low" hints and a final attempt count.
What you learn: Math.random(), conditionals, DOM updates, game state management.
Time: 1–2 hours | Difficulty: ⭐
let secret = Math.floor(Math.random() * 100) + 1;
let attempts = 0;
function guess() {
const input = document.getElementById('guess-input');
const msg = document.getElementById('message');
const val = parseInt(input.value, 10);
if (isNaN(val) || val < 1 || val > 100) {
msg.textContent = 'Enter a number between 1 and 100.';
return;
}
attempts++;
if (val < secret) msg.textContent = `Too low! (attempt ${attempts})`;
else if (val > secret) msg.textContent = `Too high! (attempt ${attempts})`;
else {
msg.textContent = `Correct! You got it in ${attempts} attempt${attempts !== 1 ? 's' : ''}.`;
document.getElementById('guess-btn').disabled = true;
}
input.value = '';
input.focus();
}
Extend it: Add a leaderboard stored in localStorage, difficulty modes, or a hint system.
#4 — Calculator
What you build: A functional calculator with +, −, ×, ÷, keyboard support, and edge-case handling (division by zero, decimal points).
What you learn: String-to-number parsing, state machines, keyboard events.
Time: 3–5 hours | Difficulty: ⭐⭐
let display = '0';
let operator = null;
let operand = null;
let waitingForSecond = false;
function updateDisplay() {
document.getElementById('display').textContent = display;
}
function inputDigit(d) {
if (waitingForSecond) { display = d; waitingForSecond = false; }
else display = display === '0' ? d : display + d;
updateDisplay();
}
function inputDot() {
if (!display.includes('.')) display += '.';
updateDisplay();
}
function setOperator(op) {
operand = parseFloat(display);
operator = op;
waitingForSecond = true;
}
function calculate() {
if (!operator || operand === null) return;
const second = parseFloat(display);
const ops = { '+': operand + second, '-': operand - second, '*': operand * second, '/': second === 0 ? 'Error' : operand / second };
display = String(ops[operator]);
operator = null;
operand = null;
updateDisplay();
}
function clear() { display = '0'; operator = null; operand = null; waitingForSecond = false; updateDisplay(); }
Extend it: Add keyboard support, history log, or scientific functions (sin, cos, √).
#5 — Colour palette generator
What you build: Click a button to generate a 5-colour palette. Click any colour to copy its hex code to the clipboard.
What you learn: Random hex generation, Clipboard API, CSS custom properties, event delegation.
Time: 2–3 hours | Difficulty: ⭐⭐
function randomHex() {
return '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0');
}
function generatePalette() {
const container = document.getElementById('palette');
container.innerHTML = '';
for (let i = 0; i < 5; i++) {
const hex = randomHex();
const div = document.createElement('div');
div.className = 'swatch';
div.style.background = hex;
div.title = 'Click to copy';
div.textContent = hex;
div.onclick = async () => {
await navigator.clipboard.writeText(hex);
div.textContent = 'Copied!';
setTimeout(() => { div.textContent = hex; }, 1500);
};
container.appendChild(div);
}
}
generatePalette();
Extend it: Add palette locking (keep one colour while regenerating others), export as CSS variables, or colour harmony modes (complementary, triadic).
#6 — Countdown timer
What you build: Enter a time in MM:SS, start a countdown that ticks down to zero and plays an alert.
What you learn: setInterval/clearInterval, time arithmetic, form input, UI state management.
Time: 2–3 hours | Difficulty: ⭐⭐
let timer = null;
let remaining = 0;
function pad(n) { return String(n).padStart(2, '0'); }
function updateDisplay() {
const m = Math.floor(remaining / 60);
const s = remaining % 60;
document.getElementById('timer-display').textContent = `${pad(m)}:${pad(s)}`;
}
function start() {
if (timer) return;
timer = setInterval(() => {
if (remaining <= 0) { clearInterval(timer); timer = null; alert('Time is up!'); return; }
remaining--;
updateDisplay();
}, 1000);
}
function reset() {
clearInterval(timer);
timer = null;
const input = document.getElementById('time-input').value.split(':');
remaining = parseInt(input[0] || 0) * 60 + parseInt(input[1] || 0);
updateDisplay();
}
Extend it: Add pause/resume, sound alerts using the Web Audio API, or a Pomodoro mode.
#7 — Quiz app
What you build: A multi-question quiz with a score tracker, instant feedback, and a final results screen.
What you learn: Arrays of objects, dynamic DOM rendering, score state, quiz flow logic.
Time: 3–4 hours | Difficulty: ⭐⭐
const questions = [
{ q: 'What does DOM stand for?', options: ['Document Object Model', 'Data Object Manager', 'Display Output Method', 'Dynamic Object Module'], answer: 0 },
{ q: 'Which method adds an element to the end of an array?', options: ['shift()', 'push()', 'pop()', 'unshift()'], answer: 1 },
{ q: 'What keyword declares a block-scoped variable?', options: ['var', 'let', 'def', 'dim'], answer: 1 },
];
let current = 0;
let score = 0;
function renderQuestion() {
const q = questions[current];
document.getElementById('question').textContent = q.q;
document.getElementById('options').innerHTML = q.options.map((opt, i) =>
`<button onclick="answer(${i})">${opt}</button>`
).join('');
}
function answer(i) {
if (i === questions[current].answer) score++;
current++;
if (current < questions.length) renderQuestion();
else document.getElementById('quiz').innerHTML = `<h2>You scored ${score}/${questions.length}</h2>`;
}
renderQuestion();
Extend it: Fetch questions from the Open Trivia Database API, add a timer per question, or save high scores.
#8 — Rock-paper-scissors
What you build: Play rock-paper-scissors against the computer, with a running score and animated result display.
What you learn: Random choices, switch statements, score tracking, CSS transitions.
Time: 2–3 hours | Difficulty: ⭐
const choices = ['rock', 'paper', 'scissors'];
let wins = 0, losses = 0, ties = 0;
function play(player) {
const computer = choices[Math.floor(Math.random() * 3)];
let result;
if (player === computer) { result = 'Tie'; ties++; }
else if (
(player === 'rock' && computer === 'scissors') ||
(player === 'paper' && computer === 'rock') ||
(player === 'scissors' && computer === 'paper')
) { result = 'You win!'; wins++; }
else { result = 'Computer wins!'; losses++; }
document.getElementById('result').textContent = `${result} (You: ${player} | Computer: ${computer})`;
document.getElementById('score').textContent = `W: ${wins} | L: ${losses} | T: ${ties}`;
}
Extend it: Add lizard and Spock variants, animate the hand icons, or implement a best-of-5 tournament mode.
Intermediate projects (#9–16)
#9 — Weather app (API)
What you build: Enter a city name, fetch current weather from the OpenWeatherMap API, and display temperature, conditions, and a weather icon.
What you learn: fetch, async/await, API keys (.env), error handling, JSON parsing.
Time: 1 day | Difficulty: ⭐⭐⭐
const API_KEY = 'YOUR_API_KEY'; // Get free key from openweathermap.org
async function getWeather(city) {
const res = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=metric`
);
if (!res.ok) throw new Error('City not found');
return res.json();
}
async function search() {
const city = document.getElementById('city-input').value.trim();
if (!city) return;
try {
const data = await getWeather(city);
document.getElementById('weather-result').innerHTML = `
<h2>${data.name}, ${data.sys.country}</h2>
<p>${Math.round(data.main.temp)}°C — ${data.weather[0].description}</p>
<img src="https://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png" alt="weather icon" />
<p>Humidity: ${data.main.humidity}% | Wind: ${data.wind.speed} m/s</p>
`;
} catch (e) {
document.getElementById('weather-result').textContent = e.message;
}
}
Extend it: Add a 5-day forecast, geolocation support, or unit toggling (°C / °F).
#10 — Expense tracker
What you build: Log income and expenses, see a running balance, and filter transactions by type.
What you learn: Array methods (reduce, filter, map), localStorage persistence, dynamic list rendering, data modelling.
Time: 1–2 days | Difficulty: ⭐⭐⭐
let transactions = JSON.parse(localStorage.getItem('transactions') || '[]');
function save() { localStorage.setItem('transactions', JSON.stringify(transactions)); }
function addTransaction(description, amount) {
transactions.push({ id: Date.now(), description, amount: parseFloat(amount) });
save();
render();
}
function deleteTransaction(id) {
transactions = transactions.filter(t => t.id !== id);
save();
render();
}
function render() {
const balance = transactions.reduce((sum, t) => sum + t.amount, 0);
const income = transactions.filter(t => t.amount > 0).reduce((s, t) => s + t.amount, 0);
const expense = transactions.filter(t => t.amount < 0).reduce((s, t) => s + t.amount, 0);
document.getElementById('balance').textContent = `$${balance.toFixed(2)}`;
document.getElementById('income').textContent = `$${income.toFixed(2)}`;
document.getElementById('expense').textContent = `$${Math.abs(expense).toFixed(2)}`;
document.getElementById('list').innerHTML = transactions.map(t => `
<li>
<span>${t.description}</span>
<span style="color: ${t.amount > 0 ? 'green' : 'red'}">${t.amount > 0 ? '+' : ''}$${t.amount.toFixed(2)}</span>
<button onclick="deleteTransaction(${t.id})">✕</button>
</li>
`).join('');
}
render();
Extend it: Add categories, a monthly summary chart using Chart.js, or CSV export.
#11 — Markdown previewer
What you build: A split-pane editor: type Markdown on the left, see the rendered HTML on the right in real time.
What you learn: marked.js library, contenteditable, XSS sanitisation with DOMPurify, debouncing.
Time: 1 day | Difficulty: ⭐⭐⭐
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
<div style="display:flex;gap:1rem">
<textarea id="editor" style="flex:1;height:80vh" placeholder="Type Markdown here...">
# Hello World
Write **bold**, *italic*, or `code`.
</textarea>
<div id="preview" style="flex:1;overflow:auto;border:1px solid #ccc;padding:1rem"></div>
</div>
<script>
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
function update() {
preview.innerHTML = DOMPurify.sanitize(marked.parse(editor.value));
}
editor.addEventListener('input', update);
update();
</script>
Extend it: Add syntax highlighting (Prism.js), export as HTML/PDF, or save drafts to localStorage.
#12 — Image search app
What you build: Search the Unsplash API for photos, display results in a masonry grid, and allow infinite scroll or pagination.
What you learn: Fetch with query params, API pagination, CSS Grid, lazy loading, infinite scroll with IntersectionObserver.
Time: 1–2 days | Difficulty: ⭐⭐⭐
const ACCESS_KEY = 'YOUR_UNSPLASH_ACCESS_KEY'; // unsplash.com/developers
let page = 1;
let query = '';
async function searchImages(q, pg = 1) {
const res = await fetch(
`https://api.unsplash.com/search/photos?query=${encodeURIComponent(q)}&page=${pg}&per_page=12&client_id=${ACCESS_KEY}`
);
return res.json();
}
async function search() {
query = document.getElementById('search-input').value.trim();
if (!query) return;
page = 1;
const data = await searchImages(query);
document.getElementById('gallery').innerHTML = data.results.map(img =>
`<img src="${img.urls.small}" alt="${img.alt_description || 'photo'}" loading="lazy" />`
).join('');
}
async function loadMore() {
page++;
const data = await searchImages(query, page);
document.getElementById('gallery').insertAdjacentHTML('beforeend',
data.results.map(img => `<img src="${img.urls.small}" alt="${img.alt_description || 'photo'}" loading="lazy" />`).join('')
);
}
Extend it: Add download buttons, lightbox modal, or a favourite-saving feature.
#13 — Note-taking app with tags
What you build: Create, tag, search, and delete notes stored in localStorage. Filter by tag.
What you learn: Complex state management, tag filtering, search/filter composition, CRUD with localStorage.
Time: 2–3 days | Difficulty: ⭐⭐⭐
let notes = JSON.parse(localStorage.getItem('notes') || '[]');
let activeTag = null;
function save() { localStorage.setItem('notes', JSON.stringify(notes)); }
function addNote(title, body, tags) {
notes.unshift({ id: Date.now(), title, body, tags: tags.split(',').map(t => t.trim()).filter(Boolean) });
save();
render();
}
function deleteNote(id) { notes = notes.filter(n => n.id !== id); save(); render(); }
function render() {
const query = document.getElementById('search').value.toLowerCase();
const visible = notes.filter(n =>
(!activeTag || n.tags.includes(activeTag)) &&
(!query || n.title.toLowerCase().includes(query) || n.body.toLowerCase().includes(query))
);
document.getElementById('notes-list').innerHTML = visible.map(n => `
<div class="note">
<h3>${n.title}</h3>
<p>${n.body}</p>
<div>${n.tags.map(t => `<span class="tag" onclick="filterTag('${t}')">#${t}</span>`).join(' ')}</div>
<button onclick="deleteNote(${n.id})">Delete</button>
</div>
`).join('');
}
function filterTag(tag) { activeTag = activeTag === tag ? null : tag; render(); }
render();
#14 — Typing speed tester
What you build: Displays a random paragraph; the user types it as fast as possible. Reports WPM and accuracy on completion.
What you learn: Keyboard events, character comparison, timer logic, string comparison, performance metrics.
Time: 1–2 days | Difficulty: ⭐⭐⭐
const passages = [
'The quick brown fox jumps over the lazy dog.',
'JavaScript was created in 10 days and has dominated the web ever since.',
'Clean code always looks like it was written by someone who cares.',
];
let startTime = null;
let passage = '';
function newTest() {
passage = passages[Math.floor(Math.random() * passages.length)];
document.getElementById('passage').textContent = passage;
document.getElementById('input').value = '';
document.getElementById('result').textContent = '';
startTime = null;
document.getElementById('input').disabled = false;
document.getElementById('input').focus();
}
document.getElementById('input').addEventListener('input', function () {
if (!startTime) startTime = Date.now();
const typed = this.value;
if (typed === passage) {
const minutes = (Date.now() - startTime) / 60000;
const words = passage.split(' ').length;
const wpm = Math.round(words / minutes);
const correct = [...typed].filter((c, i) => c === passage[i]).length;
const accuracy = Math.round((correct / passage.length) * 100);
document.getElementById('result').textContent = `${wpm} WPM | ${accuracy}% accuracy`;
this.disabled = true;
}
});
newTest();
#15 — Kanban board
What you build: A drag-and-drop task board with three columns (To Do, In Progress, Done), persisted in localStorage.
What you learn: Drag and drop API (dragstart, dragover, drop), column state, task movement between columns.
Time: 2–3 days | Difficulty: ⭐⭐⭐⭐
let board = JSON.parse(localStorage.getItem('kanban') || '{"todo":[],"inprogress":[],"done":[]}');
function save() { localStorage.setItem('kanban', JSON.stringify(board)); }
function addCard(column) {
const text = prompt('Task description:');
if (!text) return;
board[column].push({ id: Date.now(), text });
save();
render();
}
function render() {
['todo', 'inprogress', 'done'].forEach(col => {
const el = document.getElementById(col);
el.innerHTML = board[col].map(card => `
<div class="card" draggable="true" data-id="${card.id}" data-col="${col}">${card.text}</div>
`).join('');
});
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('dragstart', e => {
e.dataTransfer.setData('id', e.target.dataset.id);
e.dataTransfer.setData('col', e.target.dataset.col);
});
});
document.querySelectorAll('.column').forEach(col => {
col.addEventListener('dragover', e => e.preventDefault());
col.addEventListener('drop', e => {
const id = parseInt(e.dataTransfer.getData('id'));
const fromCol = e.dataTransfer.getData('col');
const toCol = col.id;
const card = board[fromCol].find(c => c.id === id);
if (!card || fromCol === toCol) return;
board[fromCol] = board[fromCol].filter(c => c.id !== id);
board[toCol].push(card);
save();
render();
});
});
}
render();
#16 — Currency converter
What you build: Select source and target currencies, enter an amount, and see live exchange rates fetched from a free API.
What you learn: Fetch with dynamic headers, <select> population from API data, real-time updates, error boundary UI.
Time: 1 day | Difficulty: ⭐⭐⭐
// Uses exchangerate-api.com (free tier: 1500 req/month)
const BASE_URL = 'https://api.exchangerate-api.com/v4/latest/';
let rates = {};
async function loadRates(base = 'USD') {
const res = await fetch(BASE_URL + base);
const data = await res.json();
rates = data.rates;
populateSelect('to-currency', Object.keys(rates));
}
function populateSelect(id, currencies) {
document.getElementById(id).innerHTML = currencies.map(c => `<option value="${c}">${c}</option>`).join('');
}
function convert() {
const amount = parseFloat(document.getElementById('amount').value);
const to = document.getElementById('to-currency').value;
if (isNaN(amount) || !rates[to]) return;
const result = (amount * rates[to]).toFixed(2);
document.getElementById('result').textContent = `${amount} USD = ${result} ${to}`;
}
document.getElementById('from-currency').addEventListener('change', async (e) => {
await loadRates(e.target.value);
convert();
});
loadRates();
Advanced projects (#17–25)
#17 — React task manager
What you build: A full React task manager with components, hooks (useState, useReducer, useEffect), and localStorage persistence.
What you learn: Component architecture, prop drilling vs. context, useReducer for complex state, React best practices.
Time: 2–3 days | Difficulty: ⭐⭐⭐⭐
import { useReducer, useEffect } from 'react';
const initialState = JSON.parse(localStorage.getItem('react-tasks') || '[]');
function reducer(state, action) {
switch (action.type) {
case 'ADD': return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE': return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
case 'DELETE': return state.filter(t => t.id !== action.id);
default: return state;
}
}
export default function TaskManager() {
const [tasks, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
localStorage.setItem('react-tasks', JSON.stringify(tasks));
}, [tasks]);
const [input, setInput] = React.useState('');
function submit(e) {
e.preventDefault();
if (!input.trim()) return;
dispatch({ type: 'ADD', text: input });
setInput('');
}
return (
<div>
<form onSubmit={submit}>
<input value={input} onChange={e => setInput(e.target.value)} placeholder="Add task..." />
<button type="submit">Add</button>
</form>
<ul>
{tasks.map(t => (
<li key={t.id} style={{ textDecoration: t.done ? 'line-through' : 'none' }}>
<span onClick={() => dispatch({ type: 'TOGGLE', id: t.id })}>{t.text}</span>
<button onClick={() => dispatch({ type: 'DELETE', id: t.id })}>✕</button>
</li>
))}
</ul>
</div>
);
}
#18 — REST API with Node.js + Express
What you build: A full CRUD REST API for a blog (posts, comments) with input validation, error handling, and Postman-testable endpoints.
What you learn: Express routing, middleware, HTTP methods, status codes, JSON body parsing, basic error handling.
Time: 2–3 days | Difficulty: ⭐⭐⭐⭐
import express from 'express';
const app = express();
app.use(express.json());
const posts = new Map();
let nextId = 1;
// GET /posts
app.get('/posts', (req, res) => {
res.json([...posts.values()]);
});
// POST /posts
app.post('/posts', (req, res) => {
const { title, body } = req.body;
if (!title || !body) return res.status(400).json({ error: 'title and body required' });
const post = { id: nextId++, title, body, createdAt: new Date() };
posts.set(post.id, post);
res.status(201).json(post);
});
// GET /posts/:id
app.get('/posts/:id', (req, res) => {
const post = posts.get(parseInt(req.params.id));
if (!post) return res.status(404).json({ error: 'Not found' });
res.json(post);
});
// PUT /posts/:id
app.put('/posts/:id', (req, res) => {
const post = posts.get(parseInt(req.params.id));
if (!post) return res.status(404).json({ error: 'Not found' });
const updated = { ...post, ...req.body };
posts.set(post.id, updated);
res.json(updated);
});
// DELETE /posts/:id
app.delete('/posts/:id', (req, res) => {
if (!posts.delete(parseInt(req.params.id))) return res.status(404).json({ error: 'Not found' });
res.status(204).end();
});
app.listen(3000, () => console.log('Server running on port 3000'));
#19 — Real-time chat app
What you build: A WebSocket-based chat room where multiple browser tabs can message each other in real time.
What you learn: WebSockets (ws library), bidirectional communication, broadcasting, connection management.
Time: 2–3 days | Difficulty: ⭐⭐⭐⭐
// server.js (Node.js)
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
const clients = new Set();
wss.on('connection', (ws) => {
clients.add(ws);
ws.on('message', (data) => {
const msg = data.toString();
clients.forEach(client => {
if (client.readyState === 1) client.send(msg); // 1 = OPEN
});
});
ws.on('close', () => clients.delete(ws));
});
console.log('WebSocket server on port 8080');
// client.js (browser)
const ws = new WebSocket('ws://localhost:8080');
const messages = document.getElementById('messages');
const input = document.getElementById('message-input');
ws.onmessage = (e) => {
const p = document.createElement('p');
p.textContent = e.data;
messages.appendChild(p);
messages.scrollTop = messages.scrollHeight;
};
function send() {
const text = input.value.trim();
if (!text || ws.readyState !== 1) return;
ws.send(text);
input.value = '';
}
#20 — URL shortener
What you build: Submit a long URL, get a short code back, and redirect visitors from /go/:code to the original URL.
What you learn: Express routing, in-memory or SQLite storage, redirect status codes (301/302), nanoid for short IDs.
Time: 1–2 days | Difficulty: ⭐⭐⭐⭐
import express from 'express';
import { nanoid } from 'nanoid';
const app = express();
app.use(express.json());
const db = new Map(); // { shortCode: longUrl }
app.post('/shorten', (req, res) => {
const { url } = req.body;
if (!url) return res.status(400).json({ error: 'url required' });
try { new URL(url); } catch { return res.status(400).json({ error: 'invalid URL' }); }
const code = nanoid(6);
db.set(code, url);
res.json({ short: `http://localhost:3000/go/${code}`, code });
});
app.get('/go/:code', (req, res) => {
const url = db.get(req.params.code);
if (!url) return res.status(404).send('Not found');
res.redirect(301, url);
});
app.listen(3000, () => console.log('Running on port 3000'));
#21 — Full-stack blog (Next.js + SQLite)
What you build: A full-stack blog with server-side rendering, a SQLite database, and an admin page to create/delete posts.
What you learn: Next.js App Router, Server Components, Route Handlers, better-sqlite3, Tailwind CSS.
Time: 1 week | Difficulty: ⭐⭐⭐⭐⭐
// app/api/posts/route.ts (Next.js Route Handler)
import Database from 'better-sqlite3';
import { NextResponse } from 'next/server';
const db = new Database('blog.db');
db.exec('CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)');
export function GET() {
const posts = db.prepare('SELECT * FROM posts ORDER BY created_at DESC').all();
return NextResponse.json(posts);
}
export async function POST(req: Request) {
const { title, body } = await req.json();
if (!title || !body) return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
const result = db.prepare('INSERT INTO posts (title, body) VALUES (?, ?)').run(title, body);
return NextResponse.json({ id: result.lastInsertRowid }, { status: 201 });
}
#22 — CLI task manager (Node.js)
What you build: A command-line tool (task add, task list, task done, task delete) with JSON file storage — your own mini Todoist.
What you learn: process.argv, file I/O with fs/promises, CLI UX, command routing, JSON as a database.
Time: 1–2 days | Difficulty: ⭐⭐⭐⭐
#!/usr/bin/env node
import { readFile, writeFile } from 'fs/promises';
import { existsSync } from 'fs';
const DB = './tasks.json';
async function load() {
if (!existsSync(DB)) return [];
return JSON.parse(await readFile(DB, 'utf8'));
}
async function save(tasks) {
await writeFile(DB, JSON.stringify(tasks, null, 2));
}
const [,, cmd, ...args] = process.argv;
const tasks = await load();
if (cmd === 'add') {
tasks.push({ id: Date.now(), text: args.join(' '), done: false });
await save(tasks);
console.log('Task added.');
} else if (cmd === 'list') {
tasks.forEach(t => console.log(`[${t.done ? 'x' : ' '}] ${t.id}: ${t.text}`));
} else if (cmd === 'done') {
const task = tasks.find(t => t.id === parseInt(args[0]));
if (task) { task.done = true; await save(tasks); console.log('Marked done.'); }
} else if (cmd === 'delete') {
const filtered = tasks.filter(t => t.id !== parseInt(args[0]));
await save(filtered);
console.log('Deleted.');
} else {
console.log('Usage: task add|list|done|delete [args]');
}
#23 — Browser extension (Chrome)
What you build: A Chrome extension that highlights all prices on any webpage and converts them to your chosen currency.
What you learn: Chrome Extension Manifest V3, content scripts, background service workers, chrome.storage.
Time: 2–3 days | Difficulty: ⭐⭐⭐⭐⭐
// manifest.json
{
"manifest_version": 3,
"name": "Price Converter",
"version": "1.0",
"permissions": ["storage", "activeTab", "scripting"],
"action": { "default_popup": "popup.html" },
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
}
// content.js
function highlightPrices() {
const priceRegex = /\$[\d,]+(\.\d{2})?/g;
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
const nodes = [];
while (walker.nextNode()) {
if (priceRegex.test(walker.currentNode.nodeValue)) nodes.push(walker.currentNode);
}
nodes.forEach(node => {
const span = document.createElement('span');
span.innerHTML = node.nodeValue.replace(priceRegex, match =>
`<mark style="background:yellow;font-weight:bold">${match}</mark>`
);
node.parentNode.replaceChild(span, node);
});
}
highlightPrices();
#24 — Machine learning in the browser (TensorFlow.js)
What you build: A hand gesture recogniser that uses your webcam and a pre-trained HandPose model to detect finger positions in real time.
What you learn: TensorFlow.js, Canvas API, webcam access, loading pre-trained models, real-time inference.
Time: 1–2 days | Difficulty: ⭐⭐⭐⭐⭐
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/handpose"></script>
<video id="webcam" autoplay style="display:none"></video>
<canvas id="canvas" width="640" height="480"></canvas>
<script>
async function main() {
const video = document.getElementById('webcam');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
await new Promise(r => video.onloadedmetadata = r);
const model = await handpose.load();
async function detect() {
const predictions = await model.estimateHands(video);
ctx.drawImage(video, 0, 0, 640, 480);
predictions.forEach(hand => {
hand.landmarks.forEach(([x, y]) => {
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill();
});
});
requestAnimationFrame(detect);
}
detect();
}
main();
</script>
#25 — Full-stack e-commerce (React + Node.js)
What you build: A mini e-commerce app with a product listing, cart, and checkout flow (no real payments — use Stripe test mode).
What you learn: Full-stack architecture, state management (Context or Zustand), React Router, Express backend, Stripe integration.
Time: 1–2 weeks | Difficulty: ⭐⭐⭐⭐⭐
// backend/server.js (Express + Stripe)
import express from 'express';
import Stripe from 'stripe';
import cors from 'cors';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const app = express();
app.use(cors(), express.json());
app.post('/create-checkout-session', async (req, res) => {
const { items } = req.body;
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
line_items: items.map(item => ({
price_data: {
currency: 'usd',
product_data: { name: item.name },
unit_amount: item.price * 100,
},
quantity: item.quantity,
})),
success_url: 'http://localhost:5173/success',
cancel_url: 'http://localhost:5173/cart',
});
res.json({ url: session.url });
});
app.listen(3001, () => console.log('Backend on port 3001'));
Project difficulty summary
| # | Project | Difficulty | Time | Key skill |
|---|---|---|---|---|
| 1 | Digital clock | ⭐ | 1–2h | setInterval, DOM |
| 2 | To-do list | ⭐ | 2–3h | localStorage, events |
| 3 | Number guessing | ⭐ | 1–2h | Math.random, state |
| 4 | Calculator | ⭐⭐ | 3–5h | State machine, parsing |
| 5 | Colour palette | ⭐⭐ | 2–3h | Clipboard API, CSS |
| 6 | Countdown timer | ⭐⭐ | 2–3h | setInterval, forms |
| 7 | Quiz app | ⭐⭐ | 3–4h | Arrays, dynamic DOM |
| 8 | Rock-paper-scissors | ⭐ | 2–3h | Conditionals, score |
| 9 | Weather app | ⭐⭐⭐ | 1d | fetch, async/await |
| 10 | Expense tracker | ⭐⭐⭐ | 1–2d | Array methods, data |
| 11 | Markdown previewer | ⭐⭐⭐ | 1d | Libraries, XSS |
| 12 | Image search | ⭐⭐⭐ | 1–2d | Pagination, IntersectionObserver |
| 13 | Note-taking app | ⭐⭐⭐ | 2–3d | Complex state, search |
| 14 | Typing speed tester | ⭐⭐⭐ | 1–2d | Keyboard events, metrics |
| 15 | Kanban board | ⭐⭐⭐⭐ | 2–3d | Drag & drop API |
| 16 | Currency converter | ⭐⭐⭐ | 1d | Fetch, select DOM |
| 17 | React task manager | ⭐⭐⭐⭐ | 2–3d | useReducer, context |
| 18 | REST API (Express) | ⭐⭐⭐⭐ | 2–3d | HTTP, CRUD, routing |
| 19 | Real-time chat | ⭐⭐⭐⭐ | 2–3d | WebSockets |
| 20 | URL shortener | ⭐⭐⭐⭐ | 1–2d | Express, redirects |
| 21 | Full-stack blog | ⭐⭐⭐⭐⭐ | 1wk | Next.js, SQLite, SSR |
| 22 | CLI task manager | ⭐⭐⭐⭐ | 1–2d | Node.js CLI, fs |
| 23 | Browser extension | ⭐⭐⭐⭐⭐ | 2–3d | Manifest V3, content scripts |
| 24 | TensorFlow.js app | ⭐⭐⭐⭐⭐ | 1–2d | ML inference, canvas |
| 25 | E-commerce full-stack | ⭐⭐⭐⭐⭐ | 1–2wk | React, Node, Stripe |
How to choose your first project
| Your background | Start here | Why |
|---|---|---|
| Complete beginner | #1 digital clock | Tiny scope, instant visual feedback |
| Know basic JS | #2 to-do list | Core DOM + state skills |
| Comfortable with arrays/functions | #9 weather app | First real API call |
| Learning React | #17 React task manager | Canonical first React project |
| Want a backend | #18 REST API | Node.js fundamentals |
| Portfolio focused | #21 full-stack blog | Shows full-stack breadth |
| Job application prep | #18 + #21 + #25 | Shows backend, full-stack, payments |
Portfolio tips for JavaScript projects
1. Host everything live
- Static/frontend: GitHub Pages, Netlify, Vercel (all free)
- Node.js backends: Railway, Render, Fly.io (all have free tiers)
- Full-stack (Next.js): Vercel (zero config)
A live URL is 10× more valuable than a GitHub repo link.
2. Write a clear README
Every project needs:
# Project name
One-sentence description.
## Features
- Feature 1
- Feature 2
## Tech stack
- JavaScript ES2023
- Express 4
- SQLite
## Run locally
npm install
npm start
3. Handle errors visibly
Show users what went wrong. A blank page is worse than an error message.
try {
const data = await fetchWeather(city);
renderWeather(data);
} catch (e) {
showError(`Could not fetch weather: ${e.message}`);
}
4. Use environment variables
Never hardcode API keys. Use .env files and add .env to .gitignore.
# .env
VITE_WEATHER_API_KEY=your_key_here
const key = import.meta.env.VITE_WEATHER_API_KEY; // Vite
const key = process.env.WEATHER_API_KEY; // Node.js
5. Add TypeScript to mature projects
Once a project works in JavaScript, convert it to TypeScript. This shows progression and is increasingly expected by employers.
Common mistakes to avoid
| Mistake | Why it hurts | Fix |
|---|---|---|
| Copying tutorials verbatim | No learning, no portfolio value | Modify, extend, break, rebuild |
| Starting too advanced | Discouragement kills momentum | Start with #1–4, work up |
| No error handling | App crashes on first real use | Always wrap fetch() in try/catch |
| Hard-coded API keys | Security risk, public exposure | Use .env + .gitignore |
| No README | Nobody can use your project | Write it as you build |
| One giant JS file | Hard to read, hard to debug | Split into modules (import/export) |
| Using var | Hoisting surprises, scope bugs | Always use const / let |
| Skipping mobile testing | Half your users on mobile | Test in Chrome DevTools device mode |
Frequently asked questions
How many projects do I need for a junior JavaScript job? 3–5 polished, deployed projects beat 20 half-finished ones. Employers care about depth and completeness. One great full-stack app (#21 or #25) is worth more than ten to-do lists.
Which projects impress employers most? Anything with a live URL that solves a real problem. Projects #18 (REST API), #21 (full-stack blog), and #25 (e-commerce) demonstrate the most relevant skills for junior dev roles. ML projects (#24) stand out for companies using AI.
Should I use vanilla JS or React/Vue for portfolio projects? Both. Show one or two vanilla JS projects (demonstrates fundamentals), then show one or two React projects (demonstrates modern framework skills). Avoid using React for a digital clock — it's over-engineering and looks like you don't know when to reach for a framework.
What's the best JavaScript project for complete beginners? Start with #1 (digital clock) or #3 (number guessing game) — both are under 30 lines and give immediate feedback. Then do #2 (to-do list) to learn localStorage. These three cover 80% of the DOM fundamentals you'll use in every future project.
Do I need Node.js for JavaScript projects? Not for beginner/intermediate projects — the browser gives you everything you need. Node.js becomes necessary for backend projects (#18, #19, #20, #22) and when you need to keep API keys secret. Install Node.js LTS when you reach project #9.
What if I get stuck?
- Read the browser console error message — it tells you exactly what failed and on which line. 2. Add
console.log()to trace variable values. 3. Search the exact error message on Stack Overflow or MDN. 4. Use the browser debugger (F12 → Sources → set a breakpoint). Never spend more than 30 minutes stuck — ask for help in the JS Discord or Stack Overflow.