Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Node.js HTTP Server to Understand Single-Page Application Routing and Server-Side Rendering

Tech 3

To simulate a production environment locally, a server must be built to serve a bundled application. This involves using Node.js's http module to create a server that responds to browser requests.

Two React projects are created for comparison: one using create-react-app (CRA) and another using create-next-app (Next.js). The CRA project represents a standard React single-page application (SPA) where routing is client-side. The Next.js project uses file-based routing and supports server-side rendering (SSR) and static generation.

Both projects are built:

  • CRA outputs a build directory.
  • Next.js outputs a .next directory.

A Node.js script is written to serve these built assets. The core involves the http module:

const server = require('http');

server.createServer((request, response) => {
    // Handle request and send response
}).listen(8080);

The createServer method starts a server on port 8080. When a client accesses http://127.0.0.1:8080, the server responds with resources defined in the callback.

Key steps in request handler:

  1. Parse the client's request URL to determine the requested resource path.
  2. Map the requested path to the correct file location within the built output directory.
  3. Use the fs module to read the file asynchronous.
  4. Send the file content in the response.
  5. Optionally, use child_process to automatically open a browser to the server URL.

For a Next.js application (SSR-capable), the server logic must handle dynamic routes and server-rendered pages. The built output in .next contains speicfic directories like static for assets and server-side bundles.

Example server snippet for a Next.js app:

const fileSystem = require('fs');
const httpServer = require('http');
const processUtil = require('child_process');
const urlParser = require('url');
const pathUtil = require('path');

const serverInstance = httpServer.createServer(async (incomingReq, outgoingRes) => {
    const parsedUrl = urlParser.parse(incomingReq.url, true);
    let resourcePath = parsedUrl.pathname;

    // Default to index.html for the root
    if (resourcePath === '/') {
        resourcePath = '/index.html';
    }

    // Construct absolute file path
    let absoluteFilePath = pathUtil.join(__dirname, '.next', 'static', resourcePath);

    // If file doesn't exist in static, check other Next.js specific paths (e.g., server pages)
    if (!fileSystem.existsSync(absoluteFilePath)) {
        // Logic for SSR routes or fallback
        absoluteFilePath = pathUtil.join(__dirname, '.next', 'server', 'pages', resourcePath === '/' ? 'index.html' : resourcePath + '.html');
    }

    fileSystem.readFile(absoluteFilePath, (err, data) => {
        if (err) {
            outgoingRes.writeHead(404);
            outgoingRes.end('File not found');
            return;
        }
        outgoingRes.writeHead(200);
        outgoingRes.end(data);
    });
});

serverInstance.listen(8080, () => {
    console.log('Server running on port 8080');
    processUtil.exec('start http://127.0.0.1:8080'); // Opens browser on Windows
});

For a CRA SPA, the server primarily serves index.html for all non-asset routes, enabling client-side routing. The logic is simpler as all routing is handled by React Router in the browser after the initial HTML load.

const fs = require('fs');
const http = require('http');
const url = require('url');
const path = require('path');

const port = 8081;

const spaServer = http.createServer((req, res) => {
    const parsed = url.parse(req.url);
    let filePath = '.' + parsed.pathname;

    // If path is root or a route, serve index.html
    if (filePath === './' || !path.extname(filePath)) {
        filePath = './build/index.html';
    } else {
        // Otherwise, serve static files from build directory
        filePath = './build' + parsed.pathname;
    }

    const extname = path.extname(filePath);
    let contentType = 'text/html';
    switch (extname) {
        case '.js':
            contentType = 'text/javascript';
            break;
        case '.css':
            contentType = 'text/css';
            break;
        case '.json':
            contentType = 'application/json';
            break;
        case '.png':
            contentType = 'image/png';
            break;
        case '.jpg':
            contentType = 'image/jpg';
            break;
    }

    fs.readFile(filePath, (error, content) => {
        if (error) {
            if(error.code === 'ENOENT') {
                // File not found, fallback to index.html for SPA routing
                fs.readFile('./build/index.html', (err, data) => {
                    if (err) {
                        res.writeHead(500);
                        res.end('Server Error');
                    } else {
                        res.writeHead(200, { 'Content-Type': 'text/html' });
                        res.end(data, 'utf-8');
                    }
                });
            } else {
                res.writeHead(500);
                res.end('Server Error: ' + error.code);
            }
        } else {
            res.writeHead(200, { 'Content-Type': contentType });
            res.end(content, 'utf-8');
        }
    });
});

spaServer.listen(port, () => {
    console.log(`SPA server running at http://127.0.0.1:${port}/`);
});

The difference in server logic highlights the fundamental distinction between SPAs and SSR applications. In an SPA, the server returns a single index.html file for most routes, and the client-side JavaScript handles navigation and renders components. In SSR (as with Next.js), the server can generate and return fully rendered HTML for specific routes, improving initial load performance and SEO. The Node.js HTTP server must be configured accordingly to serve the correct files and handle route-specific rendering logic.

Tags: Node.js

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.