Building a Modern HTTP Server with Promises and Async/Await

Master asynchronous JavaScript through practical server development

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
        await serveFile('index.html', 'text/html', response);
    } else if (url === '/api/data') {
        // Handle API requests
        await handleAPI(request, response);
    } 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:


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:

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
});
                

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

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!