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
- Separation of Concerns: Keep API logic, UI logic, and application state separate
- Error Handling: Always plan for failure and give users helpful feedback
- Progressive Enhancement: Start with working functionality, then add polish
- 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.