Vanilla JavaScript and Fetch - Your First Real Web App

Vanilla JavaScript and Fetch - Your First Real Web App

Before you dive into frameworks, let’s build something real with just JavaScript and the fetch API. Trust me, understanding this foundation will make you dangerous with any framework later.

Here’s what I’ve learned after years of hiring developers: the ones who understand vanilla JavaScript fundamentals adapt faster, debug more effectively, and write better code regardless of which framework they’re using. This isn’t just about learning syntax - it’s about understanding how the web actually works.

Why Start With Vanilla JavaScript?

Every framework you’ll ever use - React, Vue, Angular, Svelte - is built on top of JavaScript fundamentals. When you understand how to make HTTP requests, manipulate the DOM, and handle events without a framework, you understand what those frameworks are actually doing for you.

Plus, there are plenty of situations where you don’t need a framework. Simple interactive websites, quick prototypes, or adding dynamic behavior to existing sites - vanilla JavaScript is often the perfect tool.

The Fetch API Deep Dive

The fetch API replaced the old XMLHttpRequest and made HTTP requests much cleaner. It returns promises, works great with async/await, and handles most of the pain points that used to make AJAX development frustrating.

Basic Fetch Syntax

 1// Simple GET request
 2fetch('https://api.taskmanager.dev/v1/tasks')
 3  .then(response => response.json())
 4  .then(data => console.log(data))
 5  .catch(error => console.error('Error:', error));
 6
 7// With async/await (much cleaner)
 8async function getTasks() {
 9  try {
10    const response = await fetch('https://api.taskmanager.dev/v1/tasks');
11    const data = await response.json();
12    return data;
13  } catch (error) {
14    console.error('Error:', error);
15    throw error;
16  }
17}

Handling Different Request Types

 1// GET request with query parameters
 2async function getTasks(completed = null, priority = null) {
 3  const params = new URLSearchParams();
 4  if (completed !== null) params.append('completed', completed);
 5  if (priority) params.append('priority', priority);
 6  
 7  const url = `https://api.taskmanager.dev/v1/tasks?${params}`;
 8  const response = await fetch(url);
 9  return response.json();
10}
11
12// POST request to create a task
13async function createTask(taskData) {
14  const response = await fetch('https://api.taskmanager.dev/v1/tasks', {
15    method: 'POST',
16    headers: {
17      'Content-Type': 'application/json',
18    },
19    body: JSON.stringify(taskData)
20  });
21  
22  if (!response.ok) {
23    throw new Error(`HTTP error! status: ${response.status}`);
24  }
25  
26  return response.json();
27}
28
29// PUT request to update a task
30async function updateTask(taskId, updates) {
31  const response = await fetch(`https://api.taskmanager.dev/v1/tasks/${taskId}`, {
32    method: 'PUT',
33    headers: {
34      'Content-Type': 'application/json',
35    },
36    body: JSON.stringify(updates)
37  });
38  
39  if (!response.ok) {
40    throw new Error(`HTTP error! status: ${response.status}`);
41  }
42  
43  return response.json();
44}
45
46// DELETE request
47async function deleteTask(taskId) {
48  const response = await fetch(`https://api.taskmanager.dev/v1/tasks/${taskId}`, {
49    method: 'DELETE'
50  });
51  
52  if (!response.ok) {
53    throw new Error(`HTTP error! status: ${response.status}`);
54  }
55}

Building Our Task Manager UI

Let’s build a complete task management application step by step. We’ll start with the HTML structure, add styling, and then implement all the JavaScript functionality.

HTML Structure

 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4  <meta charset="UTF-8">
 5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6  <title>Task Manager</title>
 7  <link rel="stylesheet" href="styles.css">
 8</head>
 9<body>
10  <div class="container">
11    <header>
12      <h1>Task Manager</h1>
13      <div class="stats">
14        <span id="task-count">0 tasks</span>
15        <span id="completed-count">0 completed</span>
16      </div>
17    </header>
18
19    <form id="add-task-form" class="add-task-form">
20      <div class="form-group">
21        <input 
22          type="text" 
23          id="task-title" 
24          placeholder="What needs to be done?" 
25          required
26        >
27        <select id="task-priority">
28          <option value="low">Low Priority</option>
29          <option value="medium" selected>Medium Priority</option>
30          <option value="high">High Priority</option>
31        </select>
32      </div>
33      <div class="form-group">
34        <textarea 
35          id="task-description" 
36          placeholder="Add a description (optional)"
37          rows="2"
38        ></textarea>
39      </div>
40      <button type="submit" id="add-button">Add Task</button>
41    </form>
42
43    <div class="filters">
44      <button class="filter-btn active" data-filter="all">All</button>
45      <button class="filter-btn" data-filter="pending">Pending</button>
46      <button class="filter-btn" data-filter="completed">Completed</button>
47    </div>
48
49    <div id="loading" class="loading hidden">
50      <div class="spinner"></div>
51      <p>Loading tasks...</p>
52    </div>
53
54    <div id="error-message" class="error-message hidden"></div>
55
56    <ul id="task-list" class="task-list">
57      <!-- Tasks will be rendered here -->
58    </ul>
59  </div>
60
61  <script src="script.js"></script>
62</body>
63</html>

CSS Styling

  1* {
  2  margin: 0;
  3  padding: 0;
  4  box-sizing: border-box;
  5}
  6
  7body {
  8  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  9  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 10  min-height: 100vh;
 11  padding: 20px;
 12}
 13
 14.container {
 15  max-width: 800px;
 16  margin: 0 auto;
 17  background: white;
 18  border-radius: 10px;
 19  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
 20  overflow: hidden;
 21}
 22
 23header {
 24  background: #2c3e50;
 25  color: white;
 26  padding: 20px;
 27  display: flex;
 28  justify-content: space-between;
 29  align-items: center;
 30}
 31
 32.stats {
 33  display: flex;
 34  gap: 15px;
 35  font-size: 14px;
 36  opacity: 0.9;
 37}
 38
 39.add-task-form {
 40  padding: 20px;
 41  border-bottom: 1px solid #eee;
 42}
 43
 44.form-group {
 45  display: flex;
 46  gap: 10px;
 47  margin-bottom: 15px;
 48}
 49
 50.form-group input,
 51.form-group select,
 52.form-group textarea {
 53  flex: 1;
 54  padding: 12px;
 55  border: 1px solid #ddd;
 56  border-radius: 5px;
 57  font-size: 14px;
 58}
 59
 60.form-group select {
 61  flex: 0 0 150px;
 62}
 63
 64#add-button {
 65  background: #3498db;
 66  color: white;
 67  border: none;
 68  padding: 12px 24px;
 69  border-radius: 5px;
 70  cursor: pointer;
 71  font-size: 14px;
 72  font-weight: 500;
 73  transition: background 0.2s;
 74}
 75
 76#add-button:hover {
 77  background: #2980b9;
 78}
 79
 80#add-button:disabled {
 81  background: #bdc3c7;
 82  cursor: not-allowed;
 83}
 84
 85.filters {
 86  padding: 15px 20px;
 87  background: #f8f9fa;
 88  border-bottom: 1px solid #eee;
 89  display: flex;
 90  gap: 10px;
 91}
 92
 93.filter-btn {
 94  padding: 8px 16px;
 95  border: 1px solid #ddd;
 96  background: white;
 97  border-radius: 20px;
 98  cursor: pointer;
 99  font-size: 12px;
100  transition: all 0.2s;
101}
102
103.filter-btn.active,
104.filter-btn:hover {
105  background: #3498db;
106  color: white;
107  border-color: #3498db;
108}
109
110.loading {
111  padding: 40px;
112  text-align: center;
113  color: #666;
114}
115
116.spinner {
117  width: 40px;
118  height: 40px;
119  border: 4px solid #f3f3f3;
120  border-top: 4px solid #3498db;
121  border-radius: 50%;
122  animation: spin 1s linear infinite;
123  margin: 0 auto 15px;
124}
125
126@keyframes spin {
127  0% { transform: rotate(0deg); }
128  100% { transform: rotate(360deg); }
129}
130
131.error-message {
132  background: #e74c3c;
133  color: white;
134  padding: 15px 20px;
135  margin: 0;
136}
137
138.task-list {
139  list-style: none;
140  min-height: 200px;
141}
142
143.task-item {
144  border-bottom: 1px solid #eee;
145  padding: 20px;
146  display: flex;
147  align-items: flex-start;
148  gap: 15px;
149  transition: background 0.2s;
150}
151
152.task-item:hover {
153  background: #f8f9fa;
154}
155
156.task-item.completed {
157  opacity: 0.6;
158}
159
160.task-checkbox {
161  margin-top: 2px;
162  cursor: pointer;
163}
164
165.task-content {
166  flex: 1;
167}
168
169.task-title {
170  font-weight: 500;
171  margin-bottom: 5px;
172  cursor: pointer;
173}
174
175.task-item.completed .task-title {
176  text-decoration: line-through;
177  color: #666;
178}
179
180.task-description {
181  color: #666;
182  font-size: 14px;
183  margin-bottom: 8px;
184  line-height: 1.4;
185}
186
187.task-meta {
188  display: flex;
189  gap: 10px;
190  align-items: center;
191  font-size: 12px;
192  color: #999;
193}
194
195.priority-badge {
196  padding: 2px 8px;
197  border-radius: 10px;
198  font-weight: 500;
199  text-transform: uppercase;
200  font-size: 10px;
201}
202
203.priority-high {
204  background: #fee;
205  color: #e74c3c;
206}
207
208.priority-medium {
209  background: #fff4e6;
210  color: #f39c12;
211}
212
213.priority-low {
214  background: #f0fff4;
215  color: #27ae60;
216}
217
218.task-actions {
219  display: flex;
220  gap: 8px;
221}
222
223.delete-btn {
224  background: none;
225  border: none;
226  color: #e74c3c;
227  cursor: pointer;
228  padding: 4px 8px;
229  border-radius: 3px;
230  font-size: 12px;
231  transition: background 0.2s;
232}
233
234.delete-btn:hover {
235  background: #fee;
236}
237
238.hidden {
239  display: none;
240}
241
242.empty-state {
243  padding: 60px 20px;
244  text-align: center;
245  color: #999;
246}
247
248.empty-state h3 {
249  margin-bottom: 10px;
250  color: #666;
251}

JavaScript Implementation

Now for the JavaScript that brings everything together. We’ll organize our code into modules for better maintainability:

  1// API module for all HTTP requests
  2const API = {
  3  BASE_URL: 'https://api.taskmanager.dev/v1',
  4  
  5  async request(endpoint, options = {}) {
  6    try {
  7      const response = await fetch(`${this.BASE_URL}${endpoint}`, {
  8        headers: {
  9          'Content-Type': 'application/json',
 10          ...options.headers
 11        },
 12        ...options
 13      });
 14
 15      if (!response.ok) {
 16        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 17      }
 18
 19      return await response.json();
 20    } catch (error) {
 21      console.error('API Error:', error);
 22      throw error;
 23    }
 24  },
 25
 26  // Get all tasks with optional filters
 27  async getTasks(filters = {}) {
 28    const params = new URLSearchParams();
 29    Object.entries(filters).forEach(([key, value]) => {
 30      if (value !== null && value !== undefined) {
 31        params.append(key, value);
 32      }
 33    });
 34    
 35    const query = params.toString() ? `?${params}` : '';
 36    return this.request(`/tasks${query}`);
 37  },
 38
 39  // Create a new task
 40  async createTask(taskData) {
 41    return this.request('/tasks', {
 42      method: 'POST',
 43      body: JSON.stringify(taskData)
 44    });
 45  },
 46
 47  // Update an existing task
 48  async updateTask(taskId, updates) {
 49    return this.request(`/tasks/${taskId}`, {
 50      method: 'PUT',
 51      body: JSON.stringify(updates)
 52    });
 53  },
 54
 55  // Delete a task
 56  async deleteTask(taskId) {
 57    return this.request(`/tasks/${taskId}`, {
 58      method: 'DELETE'
 59    });
 60  }
 61};
 62
 63// UI utility functions
 64const UI = {
 65  showLoading() {
 66    document.getElementById('loading').classList.remove('hidden');
 67  },
 68
 69  hideLoading() {
 70    document.getElementById('loading').classList.add('hidden');
 71  },
 72
 73  showError(message) {
 74    const errorElement = document.getElementById('error-message');
 75    errorElement.textContent = message;
 76    errorElement.classList.remove('hidden');
 77    setTimeout(() => {
 78      errorElement.classList.add('hidden');
 79    }, 5000);
 80  },
 81
 82  formatDate(dateString) {
 83    return new Date(dateString).toLocaleDateString('en-US', {
 84      month: 'short',
 85      day: 'numeric',
 86      hour: '2-digit',
 87      minute: '2-digit'
 88    });
 89  },
 90
 91  // Create HTML for a single task
 92  createTaskHTML(task) {
 93    return `
 94      <li class="task-item ${task.completed ? 'completed' : ''}" data-id="${task.id}">
 95        <input 
 96          type="checkbox" 
 97          class="task-checkbox" 
 98          ${task.completed ? 'checked' : ''}
 99          onchange="TaskManager.toggleTask(${task.id}, this.checked)"
100        >
101        <div class="task-content">
102          <div class="task-title">${this.escapeHtml(task.title)}</div>
103          ${task.description ? `<div class="task-description">${this.escapeHtml(task.description)}</div>` : ''}
104          <div class="task-meta">
105            <span class="priority-badge priority-${task.priority}">${task.priority}</span>
106            <span>Created ${this.formatDate(task.created_at)}</span>
107          </div>
108        </div>
109        <div class="task-actions">
110          <button class="delete-btn" onclick="TaskManager.deleteTask(${task.id})">
111            Delete
112          </button>
113        </div>
114      </li>
115    `;
116  },
117
118  escapeHtml(text) {
119    const div = document.createElement('div');
120    div.textContent = text;
121    return div.innerHTML;
122  }
123};
124
125// Main TaskManager class
126class TaskManager {
127  constructor() {
128    this.tasks = [];
129    this.currentFilter = 'all';
130    this.init();
131  }
132
133  async init() {
134    this.setupEventListeners();
135    await this.loadTasks();
136  }
137
138  setupEventListeners() {
139    // Form submission
140    const form = document.getElementById('add-task-form');
141    form.addEventListener('submit', (e) => {
142      e.preventDefault();
143      this.handleAddTask();
144    });
145
146    // Filter buttons
147    document.querySelectorAll('.filter-btn').forEach(btn => {
148      btn.addEventListener('click', (e) => {
149        this.setFilter(e.target.dataset.filter);
150      });
151    });
152
153    // Auto-save on title input (debounced)
154    const titleInput = document.getElementById('task-title');
155    let titleTimeout;
156    titleInput.addEventListener('input', () => {
157      clearTimeout(titleTimeout);
158      titleTimeout = setTimeout(() => {
159        // Could implement auto-save draft functionality here
160      }, 500);
161    });
162  }
163
164  async loadTasks() {
165    try {
166      UI.showLoading();
167      const response = await API.getTasks();
168      this.tasks = response.tasks || response; // Handle different response formats
169      this.renderTasks();
170      this.updateStats();
171    } catch (error) {
172      UI.showError('Failed to load tasks. Please try again.');
173    } finally {
174      UI.hideLoading();
175    }
176  }
177
178  async handleAddTask() {
179    const title = document.getElementById('task-title').value.trim();
180    const description = document.getElementById('task-description').value.trim();
181    const priority = document.getElementById('task-priority').value;
182
183    if (!title) {
184      UI.showError('Task title is required');
185      return;
186    }
187
188    const addButton = document.getElementById('add-button');
189    addButton.disabled = true;
190    addButton.textContent = 'Adding...';
191
192    try {
193      const newTask = await API.createTask({
194        title,
195        description: description || null,
196        priority,
197        completed: false
198      });
199
200      // Add to local state
201      this.tasks.unshift(newTask);
202      
203      // Clear form
204      document.getElementById('add-task-form').reset();
205      
206      // Update UI
207      this.renderTasks();
208      this.updateStats();
209
210    } catch (error) {
211      UI.showError('Failed to create task. Please try again.');
212    } finally {
213      addButton.disabled = false;
214      addButton.textContent = 'Add Task';
215    }
216  }
217
218  async toggleTask(taskId, completed) {
219    const task = this.tasks.find(t => t.id === taskId);
220    if (!task) return;
221
222    const originalState = task.completed;
223    task.completed = completed; // Optimistic update
224
225    try {
226      await API.updateTask(taskId, { 
227        ...task, 
228        completed 
229      });
230      
231      this.renderTasks();
232      this.updateStats();
233    } catch (error) {
234      // Revert on failure
235      task.completed = originalState;
236      this.renderTasks();
237      UI.showError('Failed to update task. Please try again.');
238    }
239  }
240
241  async deleteTask(taskId) {
242    if (!confirm('Are you sure you want to delete this task?')) {
243      return;
244    }
245
246    const originalTasks = [...this.tasks];
247    
248    // Optimistic update
249    this.tasks = this.tasks.filter(t => t.id !== taskId);
250    this.renderTasks();
251    this.updateStats();
252
253    try {
254      await API.deleteTask(taskId);
255    } catch (error) {
256      // Revert on failure
257      this.tasks = originalTasks;
258      this.renderTasks();
259      this.updateStats();
260      UI.showError('Failed to delete task. Please try again.');
261    }
262  }
263
264  setFilter(filter) {
265    this.currentFilter = filter;
266    
267    // Update active filter button
268    document.querySelectorAll('.filter-btn').forEach(btn => {
269      btn.classList.toggle('active', btn.dataset.filter === filter);
270    });
271    
272    this.renderTasks();
273  }
274
275  getFilteredTasks() {
276    switch (this.currentFilter) {
277      case 'completed':
278        return this.tasks.filter(task => task.completed);
279      case 'pending':
280        return this.tasks.filter(task => !task.completed);
281      default:
282        return this.tasks;
283    }
284  }
285
286  renderTasks() {
287    const taskList = document.getElementById('task-list');
288    const filteredTasks = this.getFilteredTasks();
289
290    if (filteredTasks.length === 0) {
291      taskList.innerHTML = `
292        <div class="empty-state">
293          <h3>No tasks found</h3>
294          <p>${this.currentFilter === 'all' ? 
295            'Add your first task above to get started!' : 
296            `No ${this.currentFilter} tasks at the moment.`}
297          </p>
298        </div>
299      `;
300      return;
301    }
302
303    taskList.innerHTML = filteredTasks
304      .map(task => UI.createTaskHTML(task))
305      .join('');
306  }
307
308  updateStats() {
309    const totalTasks = this.tasks.length;
310    const completedTasks = this.tasks.filter(t => t.completed).length;
311    
312    document.getElementById('task-count').textContent = 
313      `${totalTasks} task${totalTasks !== 1 ? 's' : ''}`;
314    
315    document.getElementById('completed-count').textContent = 
316      `${completedTasks} completed`;
317  }
318}
319
320// Initialize the application when the page loads
321document.addEventListener('DOMContentLoaded', () => {
322  window.TaskManager = new TaskManager();
323});

Error Handling That Doesn’t Suck

One of the biggest differences between a demo and a production app is proper error handling. Here’s how we handle different types of errors:

Network Failures vs API Errors

 1async request(endpoint, options = {}) {
 2  try {
 3    const response = await fetch(`${this.BASE_URL}${endpoint}`, options);
 4
 5    // Handle HTTP errors (4xx, 5xx)
 6    if (!response.ok) {
 7      if (response.status === 404) {
 8        throw new Error('Task not found');
 9      } else if (response.status === 400) {
10        const errorData = await response.json();
11        throw new Error(errorData.message || 'Invalid request');
12      } else if (response.status >= 500) {
13        throw new Error('Server error - please try again later');
14      }
15      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
16    }
17
18    return await response.json();
19  } catch (error) {
20    // Handle network errors (no internet, server down, etc.)
21    if (error.name === 'TypeError') {
22      throw new Error('Network error - check your internet connection');
23    }
24    throw error;
25  }
26}

User-Friendly Error Messages

Instead of showing technical errors to users, we translate them:

 1showUserFriendlyError(error) {
 2  let message = 'Something went wrong. Please try again.';
 3  
 4  if (error.message.includes('network')) {
 5    message = 'Check your internet connection and try again.';
 6  } else if (error.message.includes('404')) {
 7    message = 'That task no longer exists.';
 8  } else if (error.message.includes('400')) {
 9    message = 'Please check your input and try again.';
10  }
11  
12  UI.showError(message);
13}

Performance Considerations

Debouncing Search Inputs

When users type in search boxes, you don’t want to make an API call on every keystroke:

 1// Debounce function
 2function debounce(func, wait) {
 3  let timeout;
 4  return function executedFunction(...args) {
 5    const later = () => {
 6      clearTimeout(timeout);
 7      func(...args);
 8    };
 9    clearTimeout(timeout);
10    timeout = setTimeout(later, wait);
11  };
12}
13
14// Usage for search
15const searchInput = document.getElementById('search');
16const debouncedSearch = debounce(async (query) => {
17  if (query.length > 2) {
18    const results = await API.getTasks({ search: query });
19    this.renderSearchResults(results);
20  }
21}, 300);
22
23searchInput.addEventListener('input', (e) => {
24  debouncedSearch(e.target.value);
25});

Caching Strategies

For data that doesn’t change often, implement simple caching:

 1const Cache = {
 2  data: new Map(),
 3  timestamps: new Map(),
 4  
 5  set(key, value, ttl = 300000) { // 5 minutes default
 6    this.data.set(key, value);
 7    this.timestamps.set(key, Date.now() + ttl);
 8  },
 9  
10  get(key) {
11    if (this.isExpired(key)) {
12      this.delete(key);
13      return null;
14    }
15    return this.data.get(key);
16  },
17  
18  isExpired(key) {
19    const expiry = this.timestamps.get(key);
20    return !expiry || Date.now() > expiry;
21  },
22  
23  delete(key) {
24    this.data.delete(key);
25    this.timestamps.delete(key);
26  }
27};
28
29// Use in API calls
30async getTasks(filters = {}) {
31  const cacheKey = `tasks-${JSON.stringify(filters)}`;
32  const cached = Cache.get(cacheKey);
33  
34  if (cached) {
35    return cached;
36  }
37  
38  const result = await this.request(`/tasks?${new URLSearchParams(filters)}`);
39  Cache.set(cacheKey, result);
40  return result;
41}

Loading States and User Feedback

Users need to know when something is happening:

 1async performAction(actionFn, loadingMessage = 'Processing...') {
 2  try {
 3    UI.showLoading(loadingMessage);
 4    const result = await actionFn();
 5    return result;
 6  } catch (error) {
 7    UI.showError(error.message);
 8    throw error;
 9  } finally {
10    UI.hideLoading();
11  }
12}
13
14// Usage
15await this.performAction(
16  () => API.createTask(taskData),
17  'Creating task...'
18);

Real-World Context and Best Practices

This is exactly how I built my first CRUD app in 2015 - no frameworks, just solid JavaScript fundamentals. These patterns still work today and form the foundation of every framework you’ll learn.

Key Takeaways

  1. Separation of Concerns: Keep API logic, UI logic, and application state separate
  2. Error Handling: Always plan for failure and give users helpful feedback
  3. Progressive Enhancement: Start with working functionality, then add polish
  4. Performance: Debounce user input, cache when appropriate, show loading states

What You’ve Learned

  • How to make HTTP requests with the fetch API
  • How to handle promises and async operations
  • DOM manipulation and event handling
  • Error handling and user feedback
  • Basic performance optimization techniques
  • Code organization and modularity

Getting Ready for Frameworks

Now that you understand how to build a complete web application with vanilla JavaScript, you’re ready to appreciate what frameworks do for you. When we build the same application with Vue.js in the next article, you’ll see how frameworks:

  • Simplify state management
  • Automate DOM updates
  • Provide better code organization
  • Handle common patterns for you

But remember - underneath every framework is JavaScript doing exactly what you’ve learned here. Understanding these fundamentals makes you a better developer no matter which tools you use.

The next step? Let’s rebuild this same application with Vue.js and see how a modern framework changes the game.