Toolmingo
Guides24 min read

25 JavaScript Projects for Beginners (with Code) — 2025 Guide

25 hands-on JavaScript projects for beginners to intermediate developers — with starter code, what you learn, difficulty ratings, and portfolio tips for 2025.

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?

  1. 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.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools