tutorial // Mar 04, 2022

How to Write a DNS Checker with Node.js

How to use the Node.js DNS package to perform a DNS lookup for a domain and build a simple UI for automating the lookup process.

How to Write a DNS Checker with Node.js

Getting Started

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Wiring up a getter for retrieving DNS records

To begin, we're going to make use of Joystick's getters to wire up the retrieval of our DNS records. In the /api folder created when running joystick create (at the root of the project), we want to create a new folder dns and within that, a file called getters.js:

/api/dns/getters.js

import DNS from 'dns';

const dns = DNS.promises;

export default {
  checkDNS: {
    input: {
      domain: {
        type: "string",
        required: true,
      },
    },
    get: async (input = {}) => {
      return {
        ipAddresses: await dns.resolve(input?.domain, 'A').catch((error) => {
          if (error?.code === 'ENODATA') {
            return [];
          }
        }),
        cname: await dns.resolve(input?.domain, 'CNAME').catch((error) => {
          if (error?.code === 'ENODATA') {
            return [];
          }
        }),
        nameserver: await dns.resolveNs(input?.domain).catch((error) => {
          if (error?.code === 'ENODATA') {
            return [];
          }
        }),
      }
    },
  },
};

Because the code we need to write is relatively simple, we've output the full file we need to write above.

First, at the top of the file, notice that we've imported DNS from the dns package. Here, dns is a built-in Node.js package for working with DNS records in a Node.js app. We've used the all-caps version for the imported value here because we want to make use of the JavaScript Promise version—not the default callback/asynchronous version—of the package's methods.

To access this, we create a new variable just below our import const dns storing the value DNS.promises (where the package stores its Promise-based API).

Moving down, from our file we export a plain JavaScript object and on it, we've added a property checkDNS set to another object. Here, checkDNS is the name of the getter we want to define. Notice that we're defining this on the parent object we're exporting, meaning, if we want we can define multiple getters in one file (we'll see how this is put to use next).

Focusing on the value set to checkDNS, on that object, we have two properties:

  1. input which describes the expected shape of the input values we anticipate being passed to our getter.
  2. get which is the function that handles or "resolves" the getter request by retrieving and returning some data.

For the input, in order to retrieve DNS information, we'll need a domain name (in the documentation for the dns package this is referred to interchangeably as the "hostname"). To input, we pass an object describing the shape of the input object we expect to receive with the request. Here, we expect a property domain and we want to validate that it contains a value with a JavaScript data type of string and that the value is present (suggested by setting required to true here).

Once our input passes validation, next, we need to wire up the get() function to actually respond to requests to our getter.

Inside of that function, as the first argument, we take in the validated input we received from the client (this is unmodified from what the client originally passes us).

Inside, we set up our code to return an object which will describe the different DNS records we care about for our domain, in particular: ipAddresses, cname, and nameserver.

To retrieve each, we put the dns package to use. Notice that in front of the function passed to get, we've added the keyword async. This tells JavaScript that we're going to use the await keyword inside of the function that keyword is prepended to in order to "wait" on the response to the function we place it in front of.

As we can see, each of our calls to dns.<method> are using the await keyword. This means that we expect those functions to return a JavaScript Promise that we want to wait on the response of. We're using two different functions from dns here:

  1. dns.resolve() which takes in a hostname as its first argument and a DNS record type as its second argument. This returns the found values for that record type as an array.
  2. dns.resolveNs() which takes in a hostname as its first argument and returns an array of DNS nameservers associated with the domain.

To retrieve any known IP addresses for our domain, we call dns.resolve() passing the A DNS record type. To retrieve any known cnames for our domain name, we pass the CNAME DNS record type.

Finally, to retrieve any known nameservers for our domain, we call dns.resolveNs() passing our domain name.

For all three calls, we want to call attention to two things. First, for our hostname value we're passing input.domain which is the domain we expect to receive from the request to our getter. Next, on the end of each function call, we've added a .catch() callback which says "if this function doesn't receive any associated data or has an error, do this." Here, "this" is checking to see if the error.code value is set to ENODATA which is the response we expect if the given DNS record can not be retrieved.

If it cannot be, we want to return an empty array (this avoids breaking the getter and signifies back to the request that no data could be found for that value).

That's it! Next, we need to actually wire this getter up to our API to ensure it's accessible.

/api/index.js

import dnsGetters from './dns/getters';

export default {
  getters: {
    ...dnsGetters,
  },
  setters: {},
};

Here, inside of /api/index.js (a file generated for us automatically when running joystick create) we've imported the object we exported from /api/dns/getters.js as dnsGetters. Below, on the object we're exporting from this file, we have two properties: getters and setters set to their own objects. Here, we define all of the getters and setters (the sibling to getters which helps us to "set" or modify data in our app).

The pattern we see here is purely organizational. In order to keep our code tidy, we put our dnsGetters in another file and then use the ... spread operator in JavaScript to "unpack" them onto the global getters object here. We say "global" because whatever we define here is handed off to Joystick in /index.server.js as the api value. Joystick uses this to generate HTTP endpoints for each of our getters and setters.

If we go ahead and save this file, to demonstrate that, if we open up a browser now and run the following, we should get a response:

http://localhost:2600/api/_getters/checkDNS?input={%22domain%22:%22cheatcode.co%22}

Notice that here, our getter was registered as an HTTP endpoint in our app automatically by joystick at /api/_getters/checkDNS.

Next, to finish up, we're going to wire up a UI component to give us a simple form for calling our getter and displaying the response back in the browser.

Wiring up a route for our UI

Before we move to the client, real quick, we want to wire up a route for the page we'll be building and create a dummy component.

/index.server.js

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => { ... },
    "/dns": (req, res) => {
      res.render("ui/pages/dns/index.js");
    },
    "*": (req, res) => { ... },
  },
});

Here, in our index.server.js file (this is the file responsible for starting our server which Joystick automatically runs for us via joystick start), to the routes object, we've added a new route /dns. Behind the scenes, Joystick will automatically register this as an Express.js route (this is what Joystick uses internally to run our HTTP server), taking the function we've passed here and using it as the "handler" for the route.

If you've ever worked with Express.js before, this is equivalent to writing something like...

app.get('/dns', (req, res) => {
  res.render('ui/pages/dns/index.js');
});

The only difference here is that Joystick gives us a standardized method for defining our routes and then automatically generates this code for us. Additionally, on the res object passed to us by Express.js, Joystick defines a special .render() function which is designed to render the Joystick component at the path we pass it.

Here, we're anticipating a Joystick component that represents a page in our app at /ui/pages/dns/index.js. Let's add a placeholder for that now:

/ui/pages/dns/index.js

import ui from '@joystick.js/ui';

const DNS = ui.component({
  render: () => {
    return `
      <div>
        <p>DNS</p>
      </div>
    `;
  },
});

export default DNS;

In that file, we import ui from @joystick.js/ui which is the "front-end" portion of the Joystick framework. On the ui object we've imported here, a method component() is defined which helps us to define a Joystick component.

On that component, we define a render function which returns a string of HTML markup that we want to represent our component. Here, to start, we're just adding a plain <div></div> tag with a <p></p> tag inside of it.

With this and index.server.js above saved, if we visit http://localhost:2600/dns in our browser, we should see the "DNS" text printed on screen.

Wiring up a UI for retrieving DNS records

Focusing back on the component skeleton we just added, now, we want to expand it to include a form for inputting a domain, a way to display the results of our call to checkDNS, and all the necessary trimmings for calling our getter via our UI.

/ui/pages/dns/index.js

import ui, { get } from '@joystick.js/ui';

const DNS = ui.component({
  state: {
    dns: null,
  },
  events: {
    'submit form': (event, component) => {
      event.preventDefault();

      get('checkDNS', {
        input: {
          domain: event?.target?.domain?.value,
        },
      }).then((response) => {
        component.setState({ dns: response });
      }).catch((error) => {
        console.warn(error);
      });
    },
  },
  css: `
    table {
      width: 50%;
    }

    table tr th,
    table tr td {
      border: 1px solid #eee;
      padding: 10px;
    }

    table tr th {
      text-align: left;
    }
  `,
  render: ({ state, when }) => {
    return `
      <div>
        <form>
          <input type="text" name="domain" placeholder="Type your domain name here..." />
          <button type="submit">Check DNS</button>
        </form>
        ${when(!!state.dns, `
          <table>
            <tbody>
              <tr>
                <th>IP Addresses</th>
                <td>${state.dns?.ipAddresses?.join(', ')}</td>
              </tr>
              <tr>
                <th>CNAMEs</th>
                <td>${state.dns?.cname?.join(', ')}</td>
              </tr>
              <tr>
                <th>Nameservers</th>
                <td>${state.dns?.nameserver?.join(', ')}</td>
              </tr>
            </tbody>
          </table>
        `)}
      </div>
    `;
  },
});

export default DNS;

This is everything we'll need. Let's walk through it. First, up at the top of our component, we've added a property state which is set to an object containing a property dns set to null. As we'll see, this is where we'll expect the data we receive back from our getter to be stored on our component. Here, we set that value dns to null as a default value.

Next, jumping down to the render function, using JavaScript destructuring to "pluck off" values from the component instance passed as the first argument to our render function, we get access to state (the value we just set the default for) and when, a "render function" (a special type of function in a Joystick component) that allows us to conditionally render some HTML in our component when some condition is met.

If we look in the HTML string returned from our render function, we can see an expression ${} being used (this is known as "interpolation" in JavaScript and allows us to pass a dynamic value inside of a JavaScript string), with a call to when() passed inside of it. To that when() function, we pass !!state.dns as the first argument, followed by a string of HTML as the second argument.

This reads: "when state.dns has a value, render this HTML." In other words, once we've retrieved our dns value from our getter and placed it on our component's state, we want to render the HTML we've passed here. That HTML, if we look close, contains an HTML <table></table> tag that renders a table outputting the DNS values we've obtained for our domain. Notice that we're using ${} interpolation again to output the contents of state.dns. From that value, too, notice that we're anticipating the same values we returned on the object from our getter on the server: ipAddresses, cname, and nameserver.

Because we expect each of those values to contain an array, to make them fit for display, we use the JavaScript .join() method to say "join all of the values in this array into a comma separated string."

In order to get to that point, up above our call to when(), we can see a simple HTML <form></form> being defined with an <input /> and a <button></button> with a type of submit.

/ui/pages/dns/index.js

events: {
  'submit form': (event, component) => {
    event.preventDefault();

    get('checkDNS', {
      input: {
        domain: event?.target?.domain?.value,
      },
    }).then((response) => {
      component.setState({ dns: response });
    }).catch((error) => {
      console.warn(error);
    });
  },
},

If we scroll up on our component, we can see how this all comes together in the events property. Here, we define JavaScript event listeners for our component. In order to handle the passing of the domain we type into our input to our getter, we define an event listener for the JavaScript submit event on our <form></form> tag.

When defining an event listener on a Joystick component, we define the event we want to listen for and the selector we want to listen for that event on using a space separated string ('<event> <selector>' ) as a property on our events object and then to that property, we assign the "handler" function to be called when that event (submit) is detected on the provided selector (form).

Inside of the function defined here, we take in two arguments: event (the JavaScript DOM event that's firing) and component (the instance for the current component).

First, because we're handling a <form></form> submission event, we want to call to event.preventDefault() to prevent the default form submission behavior in the browser (this triggers a page refresh which we want to avoid).

Next, the important part, using the get() function we've imported from @joystick.js/ui up top, we call to our checkDNS getter, passing the value of the domain input from our form (JavaScript automatically assigns inputs by their name attribute to the event.target value so we can reference them directly).

Finally, because we expect get() to return a JavaScript Promise, we add a .then() and .catch() callback onto our call. For the .then() callback, we expect to get back the value we returned from our getter as response. Using the second argument passed to our event handler component, we call its .setState() method, setting the dns value we gave a default to earlier as that response. If something goes wrong, in our .catch() callback, we log out the error to our console.

That should do it! Now, if we load up our page in the browser, we should be able to type in a domain and see the DNS information for it.

Wrapping up

In this tutorial, we learned how to wire up a simple DNS checker using the built-in dns package in Node.js. We learned how to take a domain name and pass it to various functions defined in that package, retrieving the IP addresses, nameservers, and cnames for that domain. We also learned how to wire up a Joystick component to give us a GUI for retrieving the DNS information of different domains.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode