DEV Community

Cover image for React Tutorial — City/State Lookup using the US Postal Service API

React Tutorial — City/State Lookup using the US Postal Service API

Introduction

User experience applies to every part of a website, including forms. You have to pay attention to accessibility, ease of use, and convenience. A form with good UX is easy to understand and easy to use. Who likes filling in forms? Umm, nobody! Using this thought process, I began to research what can I do to make an applicant form at the Vets Who Code website easier to use. I thought a good idea would to make the city and state self populate based on a user's U.S. Postal Code (Applicants are all veterans of US Forces). I started studying solutions. One was to use ZipCodeAPI but they charge for more than 10 requests per hour, and I am not in a position to pay for their service. Here at Vets Who Code, we like to build our own tools. I immediately thought, "How hard can it be to make my own zip code API for our use?" It appears it's not hard to get the basic functionality using the United States Postal Service's Web Tools, a 100% free, U.S. tax-payer-funded service.

Here is what we are going to be building: https://citystatelookup.netlify.app/

Goal

🔲 Build a tool using React to fetch the city and state of user based on zipcode.
🔲 Determine if entered zipcode is 5-digits.
🔲 Determine if zipcode is valid.
🔲 If the zipcode is valid, display city and state in the city/state input boxes.
🔲 Add animation as the API "loads" the city and state.

Front-end

🔲 React for building the user interface
🔲 Fetch API to GET items from the serverless function

Backend

🔲 Use Netlify Dev to create a serverless function
🔲 Process zip code to xml data and request to API
🔲 GET data from API

Prerequisites

✅ A basic understanding of HTML, CSS, and JavaScript.
✅ A basic understanding of the DOM.
✅ Yarn or npm & Nodejs installed globally.
✅ For the above three steps this overview of React by Tania Rascia is a great start. => https://www.taniarascia.com/getting-started-with-react/
✅ netlify-cli installed globally. npm i -g netlify-cli or yarn add netlify-cli
✅ Sign up for USPS Web Tools.
✅ A code editor (I'm using VS Code) I will do my best to show everything else.
✅ Netlify account.
✅ Github account.

Typing vs Copying and Pasting Code

I am a very big believer in typing code out that you intend to use for anything. Typing code versus copypasta provides a better learning return on investment because we're practicing instead of just reading. When we copy code without understanding it, we have a lesser chance of understanding what is happening. While it's nice to see our outcomes immediately the reward comes from understanding what we are doing. With that said, please don't copy and paste the code from this tutorial. Type. Everything. Out. You'll be a better programmer for it, trust me.

CORS 😈

Loading publicly accessible APIs from the frontend during development presents some problems. Mainly Cross-Origin Resource Sharing (CORS). CORS is a mechanism that uses additional HTTP headers to tell browsers to give a web application running at one origin, access to selected resources from a different origin. For security reasons, browsers restrict cross-origin HTTP requests initiated from scripts.

Ever seen this error?

CORS Policy

Setup

Going under the assumption that you have a basic understanding of HTML, CSS, and JavaScript, I am assuming you have installed npm or yarn, the latest version of node, React, netlify-cli, have a GitHub and Netlify account, and have registered to use USPS WebTools.

  1. Create a new repo on github.
  2. Create a new React site by typing npx create-react-app <new-github-repo-name>
  3. Navigate to your new folder by typing cd <new-github-repo-name>
  4. Delete all the boilerplate React code in App.js, so you're left with this:
import React from "react";
import "./App.css";

function App() {
  return <div className="App"></div>;
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  1. This is one part you are allowed to copy and paste data. Delete all the CSS code in App.css.
  2. Copy and paste the CSS code from this link => App.css.
  3. Push the code to Github to the repo you created earlier using these instructions => https://docs.github.com/en/github/importing-your-projects-to-github/adding-an-existing-project-to-github-using-the-command-line
  4. Go to app.netlify.com and login. Follow the instructions here to add your new site from Git => https://www.netlify.com/blog/2016/09/29/a-step-by-step-guide-deploying-on-netlify/

You should now be setup to start the tutorial

Frontend Form

First, let's start our development server. Type yarn start or npm start into your terminal.

Since we are trying to fetch a city and state we need to create a form.

In the code below, we set a couple states using the React useState() hooks. We also set an initial value for the cityState so it starts as an empty string.

We also added <code> so we can view our inputs as they are updated. (This can be removed later)

City and state input boxes are disabled because we do not want our user to have the ability to change it. You can also use the readonly attribute as well. The difference is minor but may make a difference depending on the end state of your form and accessibility needs. A readonly element is just not editable, but gets sent when the form submits. A disabled element isn't editable and isn't sent on submit. Another difference is that readonly elements can be focused (and getting focused when "tabbing" through a form) while disabled elements cannot.

If you notice, there is nothing to submit the form because we are going to update the city and state as the user types into the zipcode input. You will also notice that you can't actually type anything into the form. We will fix this next.

App.js

import React, { useState } from "react";
import "./App.css";

function App() {
  const initialCityState = { city: "", state: "" };
  const [cityState, setCityState] = useState(initialCityState);
  const [zipcode, setZipcode] = useState("");
  return (
    <div className="App">
      <h1>City/State Lookup Tool</h1>
      <form action="" className="form-data">
        <label htmlFor="zip">Type Zip Code Here</label>
        <input
          className="zip"
          value={zipcode}
          placeholder="XXXXX"
          type="text"
          name="zip"
          id="zip"
        />
        <label htmlFor="city">City</label>
        <input
          className={`city`}
          value={cityState.city}
          type="text"
          name="city"
          disabled
          id="city"
        />
        <label htmlFor="state">State</label>
        <input
          className={`state`}
          value={cityState.state}
          type="text"
          name="state"
          disabled
          id="state"
        />
      </form>
      <pre>
        <code>
          {JSON.stringify({
            zipcode: zipcode,
            city: cityState.city,
            state: cityState.state,
          })}
        </code>
      </pre>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

If you typed everything correctly, you should see this:

React started

Let's add a little action to this form.

We add an onChange handler to our zipcode element so that we can update the zipcode.

We destructured the value from event.target.value to make it easier to read.

We also add some validation and an input mask; this way we can insure that a user will only enter numbers and that it will only be five numbers (The length of US Postal Codes). The value.replace(/[^\d{5}]$/, "").substr(0, 5)) block has a regular expression to only allow numbers and the substr will only allow five in the form.

As you type in the form the code block at the bottom will update the zipcode.

App.js

<input
  className="zip"
  value={zipcode || ""}
  placeholder="XXXXX"
  type="text"
  name="zip"
  id="zip"
  onChange={(event) => {
    const { value } = event.target;
    setZipcode(value.replace(/[^\d{5}]$/, "").substr(0, 5));
  }}
/>
Enter fullscreen mode Exit fullscreen mode

This is what you should be left with:

zip entered gif

Netlify Functions

The previously installed netlify-cli package comes with some cool tools. One of them creates a serverless function that acts as a go between the frontend and an API that the app is trying to connect with. To interface with Netlify follow these steps:

  1. netlify init - This command is going to set off a chain of events. Firstly, it is going to ask for permission to access Netlify on your behalf. I would recommend clicking "Authorize". Close the browser and then return to your editor.
  2. Next, Netlify is going to ask if you want to create a Netlify site without a git repo. Click "No, I will connect this directory with Github first. Follow the instructions. It's going to walk you through the process of setting up a new repo and pushing it up to your repo.
  3. Type netlify init again.
  4. Select Create & configure a new site. Part of the prerequisites required creating a Netlify account. This part will log you in to Netlify. After that, select your 'team'.
  5. Name your site. It has a naming convention of only alphanumeric characters only; something like city-state-lookup-tool would work.
  6. You'll now have your partially completed app online.
  7. Next, select Authorize with Github through app.netlify.com. A new page will open asking you to allow Netlify access to your repo. Once you allow access, you can close that browser window.
  8. The Netlify tool is going to ask you the build command for your site. For yarn it CI=false yarn build, for npm it's CI=false npm run build. The CI=false flag preceding the build command will stop treating warnings as errors, which will prevent your site from being built.
  9. Directory to deploy? leave blank
  10. Netlify functions folder? type functions
  11. No netlify.toml detected. Would you like to create one with these build settings? Type Y
  12. After this a series of steps will happen and you'll end up with Success! Netlify CI/CD Configured!.

A new file should have been created named netlify.toml. If you open it up it should look similar to this:

[build]
  command = "CI=false yarn build"
  functions = "functions"
  publish: "."
Enter fullscreen mode Exit fullscreen mode

Serverless Functions

To talk to our back end without any CORS issues we need to create a serverless function. A serverless function is an app that runs on a managed server, like AWS or in this case, Netlify. The companies then manage the the server maintenance and execution of the code. They are nice because the serverless frameworks handle the go between a hosted API and the frontend application.

serverless architechture

  1. In your terminal type netlify functions:create.
  2. Typing this will create a dialog. Select node-fetch
  3. Name your function something easy to remember like getCityState. If you observe, we now have a new folder located at the root of your directory named functions. In it should be the generated file named getCityState.js with a node_modules folder, and a few other files.
  4. Open the getCityState.js file and delete the content below const fetch = require("node-fetch")

In the getCityState.js file add a couple of constants. One is for the secret key which we'll handle soon, one is for the API request link, and the last one is HTML headers which the frontend needs to handle permission to read what the function returns.

getCityState.js

const fetch = require("node-fetch");

const USER_ID = process.env.REACT_APP_USERID;
const BASE_URI =
  "http://production.shippingapis.com/ShippingAPITest.dll?API=CityStateLookup&XML=";
const config = {
  headers: {
    "Content-Type": "text/xml",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Credentials": true,
    "Access-Control-Allow-Methods": "GET",
  },
  method: "get",
};
Enter fullscreen mode Exit fullscreen mode

Below that add the main function:

getCityState.js

exports.handler = async function (event, context) {
  // The zipcode is sent by the frontend application. 
  // This is where we use it.
  const zipcode = event.queryStringParameters.zipcode;

  // The xml variable is the string we are going to send to the
  // USPS to request the information
  const xml = `<CityStateLookupRequest USERID="${USERID}"><ZipCode ID="0"><Zip5>${zipcode}</Zip5></ZipCode></CityStateLookupRequest>`;
  try {
    // Using syntactic sugar (async/await) we send a fetch request
    // with all the required information to the USPS.
    const response = await fetch(`${BASE_URI}${xml}`, config);
    // We first check if we got a good response. response.ok is
    // saying "hey backend API, did we receive a good response?"
    if (!response.ok) {
      // If we did get a good response we store the response
      // object in the variable
      return { statusCode: response.status, body: response };
    }
    // Format the response as text because the USPS response is
    // not JSON but XML
    const data = await response.text();
    // Return the response to the frontend where it will be used.
    return {
      statusCode: 200,
      body: data,
    };
    // Error checking is very important because if we don't get a
    // response this is what we will use to troubleshoot problems
  } catch (err) {
    console.log("Error: ", err);
    return {
      statusCode: 500,
      body: JSON.stringify({ msg: err.message }),
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

Add a new file named .env the root of the project and add your user information from the USPS. When you signed up they should have sent an email with this information. The title of the email should be similar to Important USPS Web Tools Registration Notice from registration@shippingapis.com

.env

In the .env file:

# USPS API Info:
REACT_APP_USERID="1234567890123"
Enter fullscreen mode Exit fullscreen mode

IMPORTANT!!!
ADD YOUR .ENV FILE TO THE .gitignore FILE

Putting it all together

Up to this point, we've created a form where we can enter a zip code, sanitized our input, created a repo on Github, connected the repo to Netlify, and created a serverless function. Now it's time to put it all together and get some info from the USPS to display the city and state of the entered zip code by "fetching" the data.

In App.js import useEffect and add the useEffect hook

App.js

import React, { useState, useEffect } from "react";

function App() {
  const initialCityState = { city: "", state: "" };
  const [cityState, setCityState] = useState(initialCityState);
  const [zipcode, setZipcode] = useState("");

  useEffect(() => {
    // Creating a new function named fetchCityState. 
    // We could have this outside the useEffect but this 
    // makes it more readable.
    const fetchCityState = async () => {
      // We are using a try/catch block inside an async function
      // which handles all the promises
      try {
        // Send a fetch request to the getCityState serverless function
        const response = await fetch(
          `/.netlify/functions/getCityState?zipcode=${zipcode}`,
          { headers: { accept: "application/json" } }
        );
        // We assign data to the response we receive from the fetch
        const data = await response.text();
        console.log(data)
        // Using a spread operator is an easy way to populate our city/state
        // form
        setCityState({...cityState, city: data, state: "" )
        // The catch(e) will console.error any errors we receive
      } catch (e) {
        console.log(e);
      }
    };
    // Run the above function
    fetchCityState();
    //The optional array below will run any time the zipcode
    // field is updated
  }, [zipcode]);
}
Enter fullscreen mode Exit fullscreen mode

Let's go ahead and restart our development server, except this time use netlify dev instead of yarn start or npm start. We're using this command now because Netlify is going to start taking over things like the connection to our getCityState serverless function.

This is what you should see:

xml object

If you type anything into the Zip Code field the <code> block below the form should update to show the city and state in the <?xml> field. Small problem though, we want to be able to use it. We'll take care of this next.

Parsing XML to JSON

There are many tools out there to parse xml to json but I wanted a native solution. Sure, many of the tools out there cover edge cases but since we know what we are getting back from the USPS, I thought a more native solution to the problem would be better. As it stands this is what we are sending to the USPS:

xml sent

<CityStateLookupRequest USERID="XXXXXXXXXXXX">
  <ZipCode ID="90210">
    <Zip5>20024</Zip5>
  </ZipCode>
</CityStateLookupRequest>
Enter fullscreen mode Exit fullscreen mode

...and this is what we receive in the response:

xml response

"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<CityStateLookupResponse><ZipCode ID=\"0\"><Zip5>90210</Zip5><City>BEVERLY HILLS</City><State>CA</State></ZipCode></CityStateLookupResponse>"
Enter fullscreen mode Exit fullscreen mode

Which is a stringified version of xml.

So how do we go about going from the stringified xml to something like this?

json

[{ "ZipCode": 910210, "City": "BEVERLY HILLS", "State": "CA" }]
Enter fullscreen mode Exit fullscreen mode

DEV to the rescue!

I followed along with this article written by Nitin Patel

niinpatel image

Link to Nitin Patel article

According to the article:

Since, XML has a lot of nested tags, this problem is a perfect example of a practical application of recursion.

An elegant solution to a difficult problem. It uses the DOMParser Web API which according to the documentation it...

parses XML or HTML to source code from a string into a DOM TREE.

Here's the function from the article:

xml2json.js

function xml2json(srcDOM) {
  let children = [...srcDOM.children];

  // base case for recursion.
  if (!children.length) {
    return srcDOM.innerHTML;
  }

  // initializing object to be returned.
  let jsonResult = {};

  for (let child of children) {
    // checking is child has siblings of same name.
    let childIsArray =
      children.filter((eachChild) => eachChild.nodeName === child.nodeName)
        .length > 1;

    // if child is array, save the values as array, 
    // else as strings.
    if (childIsArray) {
      if (jsonResult[child.nodeName] === undefined) {
        jsonResult[child.nodeName] = [xml2json(child)];
      } else {
        jsonResult[child.nodeName].push(xml2json(child));
      }
    } else {
      jsonResult[child.nodeName] = xml2json(child);
    }
  }

  return jsonResult;
}
Enter fullscreen mode Exit fullscreen mode

Let's type this into our App.js file right below the import statement.

We now have the last piece of our puzzle and should be able to parse the response from the USPS to something we can use.

Update the fetchCityState function inside the useEffect hook, and add the DOMParser

App.js

const initialCityState = { city: "", state: "" };

// Add a new DomParser API object
const parser = new DOMParser();

const [cityState, setCityState] = useState(initialCityState);
const [zipcode, setZipcode] = useState("");

useEffect(() => {
  const fetchCityState = async () => {
    try {
      const response = await fetch(
        `/.netlify/functions/getCityState?&zipcode=${zipcode}`,
        {
          headers: { accept: "application/json" },
        }
      );
      const data = await response.text();

      // Use the DOMParser here. Remember it returns a DOM tree
      const srcDOM = parser.parseFromString(data, "application/xml");

      // Use the xml2json function
      const res = xml2json(srcDOM);

      // Let's see where we're at
      console.log(res);

      // Reset the city and state to empty strings.
      setCityState({ ...cityState, city: "", state: "" });
    } catch (e) {
      console.log(e);
    }
  };
  fetchCityState();
}, [zipcode]);
Enter fullscreen mode Exit fullscreen mode

Here's what you should have in the console:

{
  "CityStateLookupResponse": {
    "ZipCode": {
      "Zip5": "90210",
      "City": "BEVERLY HILLS",
      "State": "CA"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we have something to work with! An actual object full of json-juicy-goodness Šī¸. All we have to add is some conditionals and we'll be off to the races.

Finishing up

Before we finish up let's figure out what we will need to check for:

  1. Something to check for a a valid zip code before the useEffect is run. The pseudocode would be if zip is 5-characters long, then run the useEffect.
  2. Some kind of loading conditional. useState is often used for this. We'll set the useState initially to false and in the onChange handler of the form we'll set the useState to true.
  3. Finally we have to check for errors. If the response sends back that a zip code doesn't exist, we'll let the user know in the form.

Here it is:

App.js

import React, { useEffect, useState } from "react";
import "./App.css";

const xml2json = (srcDOM) => {
  let children = [...srcDOM.children];
  // base case for recursion.
  if (!children.length) {
    return srcDOM.innerHTML;
  }
  // initializing object to be returned.
  let jsonResult = {};
  for (let child of children) {
    // checking is child has siblings of same name.
    let childIsArray =
      children.filter((eachChild) => eachChild.nodeName === child.nodeName)
        .length > 1;
    // if child is array, save the values as array, 
    // else as strings.
    if (childIsArray) {
      if (jsonResult[child.nodeName] === undefined) {
        jsonResult[child.nodeName] = [xml2json(child)];
      } else {
        jsonResult[child.nodeName].push(xml2json(child));
      }
    } else {
      jsonResult[child.nodeName] = xml2json(child);
    }
  }
  return jsonResult;
};

function App() {
  const parser = new DOMParser();

  const initialCityState = { city: "", state: "" };
  // eslint-disable-next-line
  const [cityState, setCityState] = useState(initialCityState);
  const [zipcode, setZipcode] = useState("");
  const [loading, setLoading] = useState(false);

  // We check to see if the input is 5 characters long and there
  // is something there
  const isZipValid = zipcode.length === 5 && zipcode;

  useEffect(() => {
    const fetchCityState = async () => {
      try {
        // If zip is valid then...fetch something
        if (isZipValid) {
          const response = await fetch(
            `/.netlify/functions/getCityState?&zipcode=${zipcode}`,
            {
              headers: { accept: "application/json" },
            }
          );
          const data = await response.text();
          const srcDOM = parser.parseFromString(data, "application/xml");
          console.log(xml2json(srcDOM));
          const res = xml2json(srcDOM);

          // Using optional chaining we check that all the DOM
          // items are there
          if (res?.CityStateLookupResponse?.ZipCode?.City) {
            // set loading to false because we have a result
            setLoading(false);
            // then spread the result to the setCityState hook
            setCityState({
              ...cityState,
              city: res.CityStateLookupResponse.ZipCode.City,
              state: res.CityStateLookupResponse.ZipCode.State,
            });

            // Error checking. User did not put in a valid zipcode
            // according to the API
          } else if (res?.CityStateLookupResponse?.ZipCode?.Error) {
            setLoading(false);
            // then spread the error to the setCityState hook
            setCityState({
              ...cityState,
              city: `Invalid Zip Code for ${zipcode}`,
              state: "Try Again",
            });
          }
        }
      } catch (e) {
        console.log(e);
      }
    };

    fetchCityState();
  }, [zipcode]);

  return (
    <div className="App">
      <h1>City/State Lookup Tool</h1>
      <form action="" className="form-data">
        <label htmlFor="zip">Type Zip Code Here</label>
        <input
          maxLength="5"
          className="zip"
          value={zipcode || ""}
          placeholder="XXXXX"
          type="text"
          name="zip"
          id="zip"
          onChange={(event) => {
            const { value } = event.target;
            // Set the loading to true so we show some sort of
            // progress
            setLoading(true);
            setCityState(initialCityState);
            setZipcode(value.replace(/[^\d{5}]$/, "").substr(0, 5));
          }}
        />
        <label htmlFor="city">City</label>
        <div className="input-container">
          <input
            className={`city`}
            value={cityState.city}
            type="text"
            name="city"
            disabled
            id="city"
          />
          <div className="icon-container">
            <i className={`${loading && isZipValid ? "loader" : ""}`}></i>
          </div>
        </div>
        <label htmlFor="state">State</label>
        <div className="input-container">
          <input
            className={`state`}
            value={cityState.state}
            type="text"
            name="state"
            disabled
            id="state"
          />
          <div className="icon-container">
            <i className={`${loading && isZipValid ? "loader" : ""}`}></i>
          </div>
        </div>
      </form>
      <pre>
        <code>
          {JSON.stringify({
            zipcode: zipcode,
            city: cityState.city,
            state: cityState.state,
          })}
        </code>
      </pre>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

And that's it! Run netlify dev and see your hard work payoff:

final

Conclusion

Throughout this comprehensive tutorial we covered a lot! Firstly, we set up a form using the useState hook and also normalized our zip code input. Next was writing and tying serverless function to Netlify, and Github. Finally, we parsed to response from the USPS which was sent in XML to something easier to display. All of this contributed to increasing the UX.

Vets Who Code

Did you like what you read? Want to see more? Let me know what you think about this tutorial in the comments below. As always, a donation to Vets Who Code goes to helping veteran, like myself, in learning front end development and other coding skills. You can donate here: VetsWhoCode Thanks for your time!

Top comments (0)