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?
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.
- Create a new repo on github.
- Create a new React site by typing
npx create-react-app <new-github-repo-name>
- Navigate to your new folder by typing
cd <new-github-repo-name>
- 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;
- This is one part you are allowed to copy and paste data. Delete all the CSS code in
App.css
. - Copy and paste the CSS code from this link => App.css.
- 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
- 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;
If you typed everything correctly, you should see this:
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));
}}
/>
This is what you should be left with:
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:
-
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. - 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.
- Type
netlify init
again. - 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'. - Name your site. It has a naming convention of only alphanumeric characters only; something like
city-state-lookup-tool
would work. - You'll now have your partially completed app online.
- 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. - The Netlify tool is going to ask you the build command for your site. For yarn it
CI=false yarn build
, for npm it'sCI=false npm run build
. TheCI=false
flag preceding thebuild
command will stop treating warnings as errors, which will prevent your site from being built. -
Directory to deploy?
leave blank -
Netlify functions folder?
typefunctions
-
No netlify.toml detected. Would you like to create one with these build settings?
TypeY
- 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: "."
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.
- In your terminal type
netlify functions:create
. - Typing this will create a dialog. Select
node-fetch
- 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 namedfunctions
. In it should be the generated file namedgetCityState.js
with anode_modules
folder, and a few other files. - Open the
getCityState.js
file and delete the content belowconst 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",
};
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 }),
};
}
};
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"
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]);
}
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:
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>
...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>"
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" }]
DEV to the rescue!
I followed along with this article written by Nitin Patel
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;
}
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]);
Here's what you should have in the console:
{
"CityStateLookupResponse": {
"ZipCode": {
"Zip5": "90210",
"City": "BEVERLY HILLS",
"State": "CA"
}
}
}
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:
- 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 theuseEffect
. - Some kind of loading conditional.
useState
is often used for this. We'll set theuseState
initially to false and in theonChange
handler of the form we'll set theuseState
to true. - 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;
And that's it! Run netlify dev
and see your hard work payoff:
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!