5
\$\begingroup\$

I am new to node.js, ajax, and asynchronous code. I’ve pieced together a working way to exchange json between browser and server and am wondering if someone with more experience considers the approach reasonable for my needs.

Requirement Context: This technique will be used for back-office update of data. Initially there will be one user on the same computer as the database. I’d rather not rule out allowing a few other users to remotely update the backend in the future, but if that happens, they would use simpler screens designed with the remote location in mind. Since this is back-office, I can insist on the latest Chrome browser.

My limitations: I’m a one-man shop developing/supporting a website in semi-retirement. I am part-time and my responsibilities extend beyond coding, so at best I can be a jack of some trades, but master of none.

You can skip straight to “Requirement” below, or if you want more context, read on:

Why Not Use a Library to Do This?: I am open to being told that I should, but prefer to avoid libraries unless there is compelling reason to use them. I’m a 1-man shop who needs to figure out how to do a handful of things and then automate doing them again and again. It’s a very different situation than a large company doing a multitude of things. For example, the front-end of this application has half a million static web pages written by the back-end. There are only half a dozen page types and each loads with a single hit to the server. They are written with plain vanilla html, css, and javascript. The menus would look nicer if a library were used (or when I spend more time on them), but there is value in the simplicity. Libraries complicate version control, typically provide enormous capability I will never use, and drag along legacy support I don’t need. My personal experience is that relative to many others, I am better at going a little deeper on one product than I am at remembering how multiple products interface.

Tool Rationale: There are no plans for the site to generate revenue, so one of the reasons I chose MySQL and node.js is that there is no licensing costs for a hobby I hope to continue for twenty years. The tools seem up to my requirements and are popular enough that I can find ways of doing things on the internet. Also, as a part-time, 1-man shop, being able to use the same language on the server and browser is an enormous advantage.

Requirement: With plain vanilla node.js, move large json files between browser and server to support backend data maintenance for a website. I have tested the current solution and it moves more data than I need to move more quickly than I need to move it. (I tested 100,000 objects requiring 8Meg json files both ways.) But I have no ajax experience and fear there might be problems I’m not foreseeing or easy ways to do this kind of ajax in a better way.

Issues I considered:

  • CORS requires that the server that responds to an ajax request, must first have served the html file making the request.
  • Ajax POSTs are not idempotent
  • The browser will automatically call for a favicon

On the server, the switch statement is going to get too long if I keep adding cases for each maintenance page. I’ll clean that up and welcome any general advice you’re willing to offer, but my question here is if the ajax technique is reasonable for my needs?

I’m including colorized-VSCode images of the code to make reading it easier, as well as actual code you can copy to a computer and run. The sample code can be tested as is by putting the .js and .html file in the same directory and naming them "test_ajax_post_json_sans_form”. You would test by executing "node filename" in the command console, and then loading http://localhost:8000/filename in your browser. I used Chrome.

html with client code image ORIGINAL (SEE snippet for update): enter image description here

js image: enter image description here

I included the code in snippets, but it requires node.js to run, so you'd have to copy it to a computer with node.js. (HTML UPDATED per suggestions 10/11):

'use strict';
const host = 'localhost';
const http = require('http'); // VSCode shows 3 dots under "require" and says something about NodeRequire???
const fs   = require('fs').promises;
const port = 8000;

const requestListener = function (req, res) {
    switch (req.url) {
        case "/test_ajax_post_json_sans_form.html": // serve an intial html file
            fs.readFile(__dirname + '/test_ajax_post_json_sans_form.html')
            .then(contents => {
                res.writeHead(200, {'Content-Type': 'text/html'});
                res.end(contents);
            })
            .catch(err => {
                res.writeHead(500);
                res.end(err);
                return;
            });
            break
        case '/test_ajax_post_json_sans_form.html/ajaxTest1': // receive json, process, then return other json
            let body = '';
            req.on('data', (data) => {
                body += data;
            });
            req.on('end', () => {
                console.log(body);  // to show all data has arrived
                // here we will check or errors, create a complex return-object, stringify it, and send it back
                const objToReturn = {data1: 'Test message from server', data2: 'could be a complex json object'};
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(objToReturn));
            });         
            break
        case '/favicon.ico':
            // browser will call favicon automatically. This satisfies the request (though its failing
            //   won't keep the ajax from working).
            fs.readFile(__dirname + '/favicon.ico')
            .then(contents => {
                res.writeHead(200, {'Content-Type': 'image/x-icon'});
                res.end(contents);
            })
            .catch(err => { // lacking favicon will not impact test
                res.writeHead(200, {'Content-Type': 'image/x-icon'});
                res.end();
            });
            break
        default:
            res.writeHead(404);
            res.end(JSON.stringify({error:'Resource not found'})); // in production, might load a not-found page here
    }
} 
const server = http.createServer(requestListener);
server.listen(port,host, () => { // binds the server object to a newtwork address
    console.log(`Server is running on http://${host}:${port}. (^c to cancel)`);
});
<!DOCTYPE html><body>
    <button type="button" id="submitButton">Send and then Receive JSON</button>
    <br>After clicking the button, the JSON string sent from the browser to the server will show in the console.
    <br>Then the JSON response string from the server will replace what is below.
    <br><br>
    <div id='messageArea'>
        Message to replace via ajax.
    </div>

    <script>
        'use strict';
        const submitButton = document.getElementById('submitButton');
        const messageArea = document.getElementById('messageArea');
        async function exchangeJSON() {
            try {
                submitButton.disabled = true;
                // here can build a complex object to send
                const objToSend = {message1: 'Test message from browser', message2: 'could be a complex JSON string'};
                const response = await fetch('test_ajax_post_json_sans_form.html/ajaxTest1', {
                        method: 'POST',
                        body: JSON.stringify(objToSend)
                });
                if (response.ok) {
                    const jsonResponse = await response.json();
                    // here can parse, update screen, etc.
                    messageArea.textContent = JSON.stringify(jsonResponse);
                }
                //throw new Error('Test error in ExchangeJSON'); // uncomment for testing
            } catch (error) {
                messageArea.textContent = error;
            } finally {
                submitButton.disabled = false;
            }
        }

        submitButton.addEventListener('click',exchangeJSON);
    </script>
</body></html>

\$\endgroup\$
3
\$\begingroup\$

Consider fetch In modern browsers, which your code is guaranteed to be running in, fetch is usually a better choice than XMLHttpRequest - fetch is Promise-based (Promises are usually a bit nicer to work with than callbacks), its API is a bit cleaner to read and write, and it's a bit more concise.

Error handling The front-end has no error handling. If the request fails for whatever reason, there will be no indication of that to the user - after the button is pressed, it'll appear to be processing forever, without becoming un-disabled again. Consider

  • Displaying the error message if there's an error, and
  • Re-enabling the button if there's an error

Response If you want to show the response's JSON to the user:

  • Don't set the response as innerHTML of an element. This can result in arbitrary code execution, unexpected additional HTML elements, and weird things related to HTML entities. Use .textContent instead.
  • Since the response is JSON, maybe use a more code-like element, like a <pre>?

Callbacks When you add a listener, if you want a callback to run, if the callback doesn't take any arguments, you can pass the callback directly to addEventListener instead of wrapping it in another function.

Semicolons Some of your lines are missing semicolons. To be stylistically consistent, either use them or don't - and if you choose not to, hopefully you're an expert, else you may well run into problems with automatic semicolon insertion. Choose a style, then enforce it with a linter.

idSubmitButton? The selected button's variable name should probbaly be something like submitButton - the ID isn't relevant after it's been selected. Having id as a prefix in the id attribute is strange too, maybe just use submitButton.


<button type="button" id="submitButton">Send and then Receive JSON</button>
<br>After clicking the button, the JSON string sent from the browser to the server will show in the console.
<br>Then the JSON response string from the server will replace what is below.
<br><br>
<div class='error' style='color: red; display: none;'></div>
<pre>Response gets inserted here</div>
<script>
    'use strict';
    const submitButton = document.getElementById('submitButton');
    function exchangeJSON() {
        submitButton.disabled = true; // assure post isn't sent again prior to a response
        const testObjToSend = { message1: 'Test message from browser', message2: 'could be a complex JSON string' };
        const errorDiv = document.querySelector('.error');
        errorDiv.style.display = 'none'; // Hide previous error
        // If you want to cause an error if the transaction is taking way longer than expected,
        // see https://stackoverflow.com/q/46946380
        fetch(
            'ajaxTest1',
            {
                method: 'POST',
                body: JSON.stringify(testObjToSend),
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json'
                },
            },
        )
            .then(res => res.text()) // if you wanted to expand error handling, could check if response is OK first
            .then((result) => {
                try {
                    // If it's not JSON, this will throw
                    JSON.parse(result);
                } catch(e) {
                    // Send text to catch handler below
                    throw new Error(result);
                }
                document.querySelector('pre').textContent = result;
            })
            .catch((error) => {
                errorDiv.style.display = 'block';
                errorDiv.textContent = JSON.stringify(error.message);
            })
            .finally(() => {
                submitButton.disabled = false; // post completed, so enable posting again
            });
    }
    submitButton.addEventListener('click', exchangeJSON);
</script>

Onto the backend:

Serve static files with DRY code The request and response handler for both the HTML file and the favicon are currently hard-coded into the HTTP server. While that can work, it takes an annoying amount of boilerplate code and is kind of ugly. Consider if you had 4 or 5 static files to serve instead; your current method isn't scaleable.

I prefer to avoid libraries unless there is compelling reason to use them

This is a very compelling reason to use them. Although it's true they often come with many features one doesn't care about, it's worth it for the one or two or three nontrivial features that you would otherwise have to tediously implement yourself.

Which one? I'd recommend Express, its use is very widespread, and it's well-documented on their site, on Stack Overflow, and on many other places around the internet.

Separate routes into different files Although I refactored all but one out below, for the general case when you have multiple non-static endpoints on a server that need to handle different logic, consider separating out the different routes into different files. For example, you could have one file that exports a function that handles ajaxTest1 requests, and another file that exports a function that handles login requests (just as an example). As your application grows, this is much more maintainable than putting everything into a single file.

'use strict';
const port = 8000;
const express = require('express');
// Recommended to use compression if you're transferring large files:
// https://github.com/expressjs/compression
const compression = require('compression');

const app = express();
app
    .use(compression())
    // Put static files into the "public" directory:
    .use(express.static(__dirname + '/public'))
    // Parse JSON request bodies:
    .use(express.json())
    .post('/ajaxTest1', (req, res) => {
        console.log(req.body);
        res.status(200).json({ data1: 'Test message from server', data2: 'could be a complex json object' });
    })
    .listen(port);
console.log(`Server is running on http://localhost:${port}. (^c to cancel)`);
\$\endgroup\$
  • \$\begingroup\$ Thank you CertainPerformance. This is enormously helpful! Some of this I understand immediately and other parts will take me a while to comprehend, but I wanted to express gratitude right away. \$\endgroup\$ – Jim Thompson Oct 10 '20 at 22:33
  • \$\begingroup\$ I updated the html snippet to reflect CertainPerformance suggestions to the best of my ability. It's much better. Had I presented this to begin with, would there have been other suggestions? I noticed accidentally that html ids can be accessed directly, so that it is possible (at least in Chrome) to write something like "submitButton.disable = true" rather than "document.getElementById('submitButton').disable = true". I found discussion years ago that suggested sticking with getElementById, but no recent discussion. Is it still best practice to use the verbose technique? \$\endgroup\$ – Jim Thompson Oct 11 '20 at 16:49
  • 1
    \$\begingroup\$ IDs becoming global variables happens to be something that can be relied on, but it's poor practice. If it were me, since implicit global creation can be a source of bugs, I'd avoid IDs entirely and use classes (or some other method) to select the elements instead - that's why I did const errorDiv = document.querySelector('.error');. \$\endgroup\$ – CertainPerformance Oct 11 '20 at 18:45
  • \$\begingroup\$ browser support for fetch: caniuse.com/fetch 95.61% availability \$\endgroup\$ – Nikas music and gaming Dec 24 '20 at 19:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.