Understanding Asynchronous Programming
Imagine you're at a busy restaurant. When you place your order, the waiter doesn't stand at your table waiting for the kitchen to prepare your food. Instead, they take your order (like making a Promise), continue serving other tables (executing other code), and return with your food when it's ready (resolving the Promise). This is exactly how asynchronous programming works in JavaScript!
In our web server, we'll be handling multiple requests simultaneously, just like a waiter managing multiple tables. Each request is like a new customer walking into the restaurant - we need to handle them efficiently without making others wait unnecessarily.
Setting Up Our Project
Let's create our server piece by piece. First, we'll need Node.js installed on our system. Think of Node.js as our restaurant building - it provides the space and basic utilities we need to run our server.
// server.js
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
const PORT = 3000;
Creating Our First Server
Here's our basic server setup, similar to opening our restaurant for the first time:
const server = http.createServer(async (request, response) => {
try {
// Log incoming requests (like a hostess greeting customers)
console.log(`Incoming ${request.method} request to: ${request.url}`);
// Handle the request
await handleRequest(request, response);
} catch (error) {
// Error handling (like dealing with customer complaints)
console.error('Server Error:', error);
response.writeHead(500);
response.end('Internal Server Error');
}
});
server.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
});
Request Handler - The Heart of Our Server
Think of the request handler as our master chef - it determines what needs to be prepared based on the customer's order (request URL):
async function handleRequest(request, response) {
const { url, method } = request;
// Serve different content based on URL (like different menu items)
if (url === '/') {
// Serve home page
const homePage = `
Modern Node.js Server
Welcome to Your Server
This is a demonstration of a modern Node.js server with async/await and fetch capabilities.
Data Display
Click "Fetch Data" to load content...
File Upload
Server Status
Server is running on port ${PORT}
`;
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end(homePage);
} else if (url === '/api/data') {
// Handle API requests
await handleAPI(request, response);
} else if (url === '/api/upload' && method === 'POST') {
// Handle file uploads
await handleFileUpload(request, response);
} else if (url === '/api/status') {
// Handle status checks
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ status: 'operational' }));
} else {
// Try to serve static files
try {
const filePath = path.join('public', url);
await serveFile(filePath, getContentType(url), response);
} catch {
response.writeHead(404);
response.end('Not Found');
}
}
}
File Server - Our Kitchen Storage
Just as a restaurant needs an organized storage system for ingredients, our server needs a way to manage and serve files:
async function serveFile(filePath, contentType, response) {
try {
const data = await fs.readFile(filePath);
response.writeHead(200, { 'Content-Type': contentType });
response.end(data);
} catch (error) {
throw new Error(`Error serving ${filePath}: ${error.message}`);
}
}
function getContentType(url) {
const extensionMap = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg'
};
return extensionMap[path.extname(url)] || 'text/plain';
}
API Handler - Our Special Orders
Like handling special dietary requirements or custom orders, our API handler manages special data requests:
// Handle file uploads
async function handleFileUpload(request, response) {
try {
let fileData = Buffer.from([]);
request.on('data', chunk => {
fileData = Buffer.concat([fileData, chunk]);
});
await new Promise((resolve, reject) => {
request.on('end', resolve);
request.on('error', reject);
});
// In a real application, you'd save the file here
// For demo purposes, we'll just acknowledge receipt
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({
message: 'File received',
size: fileData.length
}));
} catch (error) {
console.error('Upload error:', error);
response.writeHead(500);
response.end(JSON.stringify({ error: 'Upload failed' }));
}
}
async function handleAPI(request, response) {
const { method } = request;
if (method === 'GET') {
// Simulate database fetch
await new Promise(resolve => setTimeout(resolve, 100));
const data = { message: 'Hello from the API!' };
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify(data));
} else if (method === 'POST') {
let body = '';
// Collect data chunks (like gathering ingredients)
request.on('data', chunk => {
body += chunk.toString();
});
// Process the complete request
await new Promise((resolve, reject) => {
request.on('end', resolve);
request.on('error', reject);
});
// Process the data (like preparing a meal)
const data = JSON.parse(body);
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ received: data }));
}
}
Real-World Applications
This server architecture forms the foundation of many real-world applications:
- Content Management Systems (CMS) - Serving different types of content based on URLs
- REST APIs - Handling data requests for web and mobile applications
- File Hosting Services - Managing and serving various file types
- Web Applications - Serving HTML, CSS, and JavaScript files
Error Handling and Logging
Just as a restaurant needs systems to handle spills, complaints, and accidents, our server needs robust error handling:
// Add this to your server setup
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// Log to file or monitoring service in production
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Log to file or monitoring service in production
});
Understanding Fetch API
Think of the Fetch API as a courier service in our restaurant analogy. Just as a delivery service picks up food from restaurants and delivers it to customers, fetch retrieves data from servers and delivers it to our client applications.
// Basic GET request
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// Same request using async/await
async function getData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
Advanced Fetch Usage
Like a restaurant offering different delivery options (express, scheduled, or special handling), fetch supports various request configurations:
// POST request with custom headers and body
async function postData(url, data) {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token-here'
},
body: JSON.stringify(data)
};
try {
const response = await fetch(url, options);
// Check if the request was successful
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// Example usage with different request methods
async function apiExamples() {
try {
// PUT request
const updateResponse = await fetch('/api/update', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 1, status: 'updated' })
});
// DELETE request
const deleteResponse = await fetch('/api/delete/1', {
method: 'DELETE'
});
// Request with timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const timeoutResponse = await fetch('/api/data', {
signal: controller.signal
});
clearTimeout(timeoutId);
// Handling file uploads
const formData = new FormData();
formData.append('file', fileInput.files[0]);
const uploadResponse = await fetch('/api/upload', {
method: 'POST',
body: formData
});
} catch (error) {
console.error('API Error:', error);
}
}
Error Handling with Fetch
Just as a delivery service needs to handle various situations (wrong address, closed restaurant, traffic delays), we need robust error handling for our fetch requests:
async function robustFetch(url, options = {}) {
// Default retry configuration
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // ms
let lastError = null;
for (let i = 0; i < MAX_RETRIES; i++) {
try {
const response = await fetch(url, {
...options,
// Add default headers
headers: {
'Content-Type': 'application/json',
...options.headers,
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
lastError = error;
// Don't retry on certain errors
if (error.name === 'AbortError' ||
(error.response && error.response.status === 404)) {
throw error;
}
// Wait before retrying
if (i < MAX_RETRIES - 1) {
await new Promise(resolve =>
setTimeout(resolve, RETRY_DELAY * (i + 1))
);
}
}
}
throw lastError;
}
// Usage example
async function fetchWithRetry() {
try {
const data = await robustFetch('/api/data', {
method: 'POST',
body: JSON.stringify({ key: 'value' })
});
console.log('Success:', data);
} catch (error) {
console.error('Final error:', error);
}
}
Real-World Fetch Patterns
Common patterns you'll encounter in production applications:
- Authentication - Handling tokens and session management
- Request/Response Interceptors - Modifying requests and responses globally
- Cache Management - Implementing caching strategies
- Rate Limiting - Managing API request frequency
- Request Queuing - Handling multiple requests efficiently
- Progress Monitoring - Tracking upload/download progress
Testing Our Server
Here's a simple test script to verify our server's functionality:
// test.js
async function testServer() {
try {
// Test GET request
const response = await fetch('http://localhost:3000/api/data');
const data = await response.json();
console.log('GET response:', data);
// Test POST request
const postResponse = await fetch('http://localhost:3000/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ test: 'data' })
});
const postData = await postResponse.json();
console.log('POST response:', postData);
} catch (error) {
console.error('Test failed:', error);
}
}
testServer();
Further Topics to Explore
- HTTP/2 and HTTP/3 - Modern protocols for faster web communication
- WebSockets - Real-time bidirectional communication
- Server-Sent Events - Push updates from server to client
- Authentication and Authorization - Securing your server
- Rate Limiting - Protecting your server from abuse
- Caching Strategies - Improving performance
- Load Balancing - Distributing traffic across multiple servers
Conclusion
Building a web server with async/await and promises makes our code more readable and maintainable. Just as a well-organized restaurant can serve many customers efficiently, our server can handle multiple requests smoothly. Keep practicing and experimenting with different features to become a master of server-side JavaScript!