Search

Mailing A Postcard With JavaScript Part 2: Verifying an Address and Mailing a Postcard

This is the second article in our three-part series about using Lob APIs to build an app to create and send postcards. In part one , we set up our application in Vue and Node. We also enabled our users to generate and save ready-to-mail postcards as Lob HTML Templates. Finally, we synced those templates with the Lob API.

We’ll now improve our application by enabling our users to send physical postcards to their customers. We’ll accept addresses, verify them (on both the client and server-side), then queue our postcard for delivery.

Review the first article of this series to follow along with this tutorial. Let’s dive in!

Improving our app

We’ll build on the application we started last time. If you’re coding along, make sure you have that application ready to go.

Let’s first create an AddressForm component to use in our application. We need to get the address of both our sender and our recipient to send to our server, so we’ll use this component at least twice. We’ll accept two props: a reactive address object that we can share with the parent, and a form ID. Create a new file called “AddressForm.vue” in the “frontend/src/components” folder.

				
					<script setup>
import { toRefs, ref, onMounted } from "vue";


const props = defineProps({
	address: Object,
	formId: String
})

const { name, address_line1, address_line2, address_city, address_state, address_zip } = toRefs(props.address)

onMounted(()=> { console.log(&lsquo;onmounted hook&rsquo;) })
</script>
				
			
We’ll destructure individual elements from our incoming prop. We need to use the toRefs function to help us do this. If we don’t, the destructured values won’t be reactive, meaning we can’t share them with the parent. Let’s now use these values to set up our form:
				
					<template>
	<form :id="formId">
		<div class="form-field">
			<label for="name">Name</label>
			<input id="name" v-model="name" />
		</div>
		<div class="form-field">
			<label for="firstLine">Address Line 1</label>
			<input id="firstLine" v-model="address_line1" />
		</div>
		<div class="form-field">
			<label for="secondLine">Address Line 2</label>
			<input id="secondLine" v-model="address_line2" />
		</div>
		<div class="form-field">
			<label for="city">City</label>
			<input id="city" v-model="address_city" />
		</div>
		<div class="form-field">
			<label for="state">State</label>
			<input id="state" v-model="address_state" />
		</div>
		<div class="form-field">
			<label for="zip">Zip Code</label>
			<input id="zip" v-model="address_zip" />
		</div>
	</form>
</template>
				
			

Next, let’s create a parent page to use this component and select templates for our postcard’s front and back. Create a file named “CreatePostcard.vue” in the same folder as our previous component.

				
					<script setup>
import { ref, onMounted } from "vue";
import AddressForm from "./AddressForm.vue";

const toAddress = ref({ name: "", address_line1: "", address_line1: "", address_city: "", address_state: "", address_zip: "" })
const fromAddress = ref({ name: "", address_line1: "", address_line1: "", address_city: "", address_state: "", address_zip: "" })
const templates = ref([]);
const frontTemplate = ref("")
const backTemplate = ref("")
const error = ref("")
const success = ref(false)
const loading = ref(false)
const frontThumbnail = ref("")
const backThumbnail = ref("")


onMounted(() => {
	fetch("http://localhost:3030/templates")
		.then((data) => data.json())
		.then((data) => templates.value = data);
})
</script>
				
			
In our script section, we get our reactive variables ready. We have an object and starting values for each of our addresses, an array of templates, the ID of the front and back templates/thumbnails, and any possible error messages. We use the onMounted lifecycle function to fetch the templates when our page first loads so our users can select from templates they have stored in Lob.
				
					<template>
   <div>
   <div v-if="loading == true">LOADING...</div>
   <div v-if="error" class="error">{{ error }}</div>
   <div v-if="success == true">
       <div class="success">Postcard Created!</div>
       <div class="flex">
           <div style=""><img loading="lazy" :src=frontThumbnail /><br /><span>Front</span></div>
           <div><img loading="lazy" :src=backThumbnail /><br /><span>Back</span></div>
       </div>
   </div>
   <h2>Select a template:</h2>
   <div class="flex">
       <select v-model="frontTemplate" class="select">
           <option value>Please select front template</option>
           <option v-for="template in templates" :value="template.id">{{ template.description }}</option>
       </select>
       <select v-model="backTemplate" class="select">
           <option value>Please select back template</option>
           <option v-for="template in templates" :value="template.id">{{ template.description }}</option>
       </select>
   </div>
   <div class="container">
       <div class="address">
           <h2>Address you're sending to</h2>
           <AddressForm :address="toAddress" formId="toAddress" />
       </div>
       <div class="address">
           <h2>Address you're sending from</h2>
           <AddressForm :address="fromAddress" formId="fromAddress" />
       </div>
   </div>
 </div>
</template>
<style lang="scss">


.container {
   display: flex;
   justify-content: space-between;
}

.flex {
   display: flex;
   justify-content: center;
}

.strikeout {
   text-decoration: line-through;
}

.address {
   width: 100%;
   margin: 14px;
}

.submit-area {
   border: 1px solid black;
   padding: 4px;
   padding-bottom: 16px;
   width: 50%;
   margin: auto;
}

.error {
   border: 1px solid;
   margin: 10px 0px;
   padding: 15px 10px 15px 50px;
   background-repeat: no-repeat;
   background-position: 10px center;
   color: #d8000c;
   background-color: #ffbaba;
   background-image: url("https://i.imgur.com/GnyDvKN.png");
}

.success {
   border: 1px solid;
   margin: 10px 0px;
   padding: 15px 10px 15px 50px;
   color: #ffffff;
   background-color: #8af89c;
}

:root {
   --select-border: #777;
   --select-focus: blue;
   --select-arrow: var(--select-border);
}

select {
   appearance: none;
   background-color: transparent;
   border: none;
   padding: 0 1em 0 0;
   margin: 0;
   width: 100%;
   font-family: inherit;
   font-size: inherit;
   cursor: inherit;
   line-height: inherit;

   // Stack above custom arrow
   z-index: 1;

   // Remove dropdown arrow in IE10 & IE11
   // @link https://www.filamentgroup.com/lab/select-css.html
   &::-ms-expand {
       display: none;
   }

   // Remove focus outline, will add on alternate element
   outline: none;
}

.select {
   display: grid;
   grid-template-areas: "select";
   align-items: center;
   position: relative;
   margin: 4px;

   select,
   &::after {
       grid-area: select;
   }

   min-width: 15ch;
   max-width: 30ch;

   border: 1px solid var(--select-border);
   border-radius: 0.25em;
   padding: 0.25em 0.5em;

   font-size: 1.25rem;
   cursor: pointer;
   line-height: 1.1;

   // Optional styles
   // remove for transparency
   background-color: #fff;
   background-image: linear-gradient(to top, #f9f9f9, #fff 33%);

   // Custom arrow
   &:not(.select--multiple)::after {
       content: "";
       justify-self: end;
       width: 0.8em;
       height: 0.5em;
       background-color: var(--select-arrow);
       clip-path: polygon(100% 0%, 0 0%, 50% 100%);
   }
}

// Interim solution until :focus-within has better support
select:focus + .focus {
   position: absolute;
   top: -1px;
   left: -1px;
   right: -1px;
   bottom: -1px;
   border: 2px solid var(--select-focus);
   border-radius: inherit;
}

select[multiple] {
   padding-right: 0;

   /*
  * Safari will not reveal an option
  * unless the select height has room to
  * show all of it
  * Firefox and Chrome allow showing
  * a partial option
  */
   height: 6rem;

   option {
       white-space: normal;

       // Only affects Chrome
       outline-color: var(--select-focus);
   }

   /*
  * Experimental - styling of selected options
  * in the multiselect
  * Not supported crossbrowser
  */
   //   &:not(:disabled) option {
   //     border-radius: 12px;
   //     transition: 120ms all ease-in;

   //     &:checked {
   //       background: linear-gradient(hsl(242, 61%, 76%), hsl(242, 61%, 71%));
   //       padding-left: 0.5em;
   //       color: black !important;
   //     }
   //   }
}

.select--disabled {
   cursor: not-allowed;
   background-color: #eee;
   background-image: linear-gradient(to top, #ddd, #eee 33%);
}

label {
   font-size: 1.125rem;
   font-weight: 500;
}

.select + label {
   margin-top: 2rem;
}
.form-field {
   display: flex;
   flex-direction: column;
}

.form-field label {
   flex: 1;
   text-align: left;
}

.form-field input {
   flex: 1;
   width:100%;
}

</style>
				
			

In our template, we provide selects to allow our user to pick their templates. We also render the AddressForm twice, once for the sender and once for the recipient. Notice that we use the “lang” attribute on the “style” element. Since we are referencing Sass, we need to install the vue-loader that will handle the preprocessing for us. In the terminal, at the root of the “frontend” folder, run the following command:

Npm install -D sass-loader sass

The final step is to give our new page a route, so let’s head over to the “frontend/src/router/index.js” file and modify this file so that looks like this:

				
					import { createWebHistory, createRouter } from "vue-router";
import ListTemplates from "../components/ListTemplates.vue";
import Front from "../components/Front.vue";
import CreatePostcard from "../components/CreatePostcard.vue"
const routes = [
 { path: "/", component: Front, name: "Home" },
 { path: "/list", component: ListTemplates, name: "ListTemplates" },
 { path: "/create", component: CreatePostcard, name: "CreatePostcard"}
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;
				
			
We next use Lob’s client-side library, Address Elements, to verify and autocomplete US addresses in the browser. The app needs to load this library after the forms render. Then it searches for the correct fields and allows autocompletion as necessary. Back in our parent component, we add this script’s mounting to our onMounted function.
				
					onMounted(() => {
   fetch("http://localhost:3030/templates")
       .then((data) => data.json())
       .then((data) => templates.value = data);
  
   const script = document.createElement("script");
   script.src = "https://cdn.lob.com/lob-address-elements/2.1.3/lob-address-elements.min.js";
   script.async = true;
   script.setAttribute("data-lob-key", import.meta.env.VITE_LOB_API_KEY);
   script.setAttribute("data-lob-primary-id", "firstLine");
   script.setAttribute("data-lob-secondary-id", "secondLine");
   script.setAttribute("data-lob-city-id", "city");
   script.setAttribute("data-lob-state-id", "state");
   script.setAttribute("data-lob-zip-id", "zip");
   document.body.appendChild(script);

})
				
			
This function works great, updating the form as we’d expect. But, it doesn’t update the reactive variables. To handle that action, we need to subscribe to an event that the library makes available, then revise based on that event. We will need to update the “.env” file that is the root of the “frontend” folder with the API keys that Lob provides us. For the address verification to work, we will need to use the “live” public keys as the “test” keys do not offer address completion. Add an entry that has the following format: VITE_LOB_API_KEY=<insert_live_publishable_api_key> In our AddressForm component, we add a new ref for our subscription and an event listener to our window. We do this because we can’t guarantee that the LobAddressElements library will be ready when the app mounts this component. We’ll listen for the keydown event and return early if we have the subscription or LobAddressElements isn’t available. In the “frontend/src/components/AddressForm.vue” let’s add the following pieces of code:
				
					import { toRefs, ref, onMounted } from "vue";
const props = defineProps({
   address: Object,
   formId: String
})
const { name, address_line1, address_line2, address_city, address_state, address_zip } = toRefs(props.address)
const subscription = ref();
onMounted(() => {
   window.addEventListener("keydown", () => {
       if (subscription.value || !window.LobAddressElements) return
       subscription.value = window.LobAddressElements.on('elements.us_autocompletion.selection', function (payload) {

           if (payload.form.id !== props.formId) return

           const { selection: {
               primary_line, city, state, zip_code
           }
           } = payload

           address_line1.value = primary_line
           address_city.value = city
           address_state.value = state
           address_zip.value = zip_code
     });
  });
});
				
			
If we make it past that conditional, we subscribe to the elements.us_autocompletion.selection event and update our state if it’s targeting the correct form. And just like that, our address forms have autocompletion and address verification.

Next, we prepare our payload and enable our users to submit their requests to the app’s backend. Place this in the “CreatePostcard” component:

				
					async function handleSubmit() {
   const body = {
       toAddress: { ...toAddress.value },
       fromAddress: { ...fromAddress.value },
       frontTemplate: frontTemplate.value,
       backTemplate: backTemplate.value
   }
   error.value = ""
   success.value = false
   loading.value = true
   const response = await fetch("http://localhost:3030/postcard/create", {
       method: "POST",
       body: JSON.stringify(body),
       headers: {
           "Content-Type": "application/json"
       }
   })

   const data = await response.json()

   if (!data.success) {
       loading.value = false
       error.value = data.error_message
       return
   }

   setTimeout(function(){
       loading.value = false
       backThumbnail.value = data.postcard.thumbnails[1].medium
       frontThumbnail.value = data.postcard.thumbnails[0].medium
       success.value = true
   }, 4000)

}
				
			

Note the use of .value to access the underlying value of the reference object while we’re inside our script tag. You will notice the “setTimeout” function that wraps the code path if the request is successful. This is because rendering thumbnails is an asynchronous task in Lob and depending on when you go to thumbnail link, the task may or may not have been completed. There is actually a webhook event that you could subscribe to called “postcard.rendered_thumbnails” that will let you know when the task is complete. Stay tuned for future tutorials where we will go over subscribing and ingesting events via webhooks.

We also have to add the submit button for our form, so after the “container” class we will add the following to the “CreatePostcard” component:

				
					<div class="submit-area">
       <h2>Ready to go?</h2>
       <button class="btn_submit" @click="handleSubmit()">Submit</button>
</div>
				
			

Building a handler

We first need to enable our server to parse the JSON that we’ll be sending in our body on our backend. Express comes with an inbuilt helper function we can use for this, so in our “backend/index.js”  file, we will use the JSON middleware. Add this after the line that has “app.use(cors())”:

				
					app.use(express.json());
				
			

Now, we need to build the handler function. Before we start with the rest of the backend code, we need to install the Lob SDK via npm. In the terminal type following command (making sure, you are in the “backend” folder for the project):

				
					npm install --save lob
				
			

Let’s create a new file at “postcard/index.js”. We will use the Lob SDK for Node.js to build our handler. We import the SDK then instantiate it with our API key. Add the following to “postcard/create.js”:

				
					import L from "lob";

export default async function createPostcard(req, res) {
  const Lob = L(process.env.LOB_SECRET_API_KEY);
}
				
			
The following steps will fill in the “createPostcard” function. We use the Lob.postcards.create method to verify our addresses during that operation and queue our postcard for sending. This method takes two parameters: the options object, and a callback function. We pass in our options, then in the callback function, we check if there is an error. We get helpful error messages back from the API, but they’re nested. We do some restructuring to make it easier for our front end to consume these messages. If there is no error, we return a success message and the newly created postcard object that was sent to us from the Lob API. We will use this object to show a preview of what the postcard will look like on the frontend. Place the following code inside the “createPostcard” function.
				
					   const { toAddress, fromAddress, frontTemplate, backTemplate, description } = req.body;

Lob.postcards.create(
   {
       description: description,
       to: toAddress,
       from: fromAddress,
       front: frontTemplate,
       back: backTemplate,
   },
   function (err, postcard) {
       if (err) {
           return res.status(err.status_code || 500).send({
               success: false,
               error_message:
                   err?._response?.body?.error?.message ||
                   err.message ||
                   "Unknown error.",
               });     
       } else {
           res.send({ success: true, postcard: postcard});
       }
   })
				
			
It’s possible to check the addresses separately at this stage if we’d prefer. The Lob.usVerifications.verify() method is powerful. The API takes a slightly different structure for the address argument so that it’ll need a little restructuring:
				
					 Lob.usVerifications.verify(
          {
            primary_line: toAddress.address_line1,
            city: toAddress.address_city,
            state: toAddress.address_state,
          },
          function (err, res) {
            if (err) reject(new Error(err));
            resolve(res);
          }
        );
				
			
The response from the verification API is detailed and helpful. We get back a confidence score, a deliverability enum, and some deliverability analysis. This API doesn’t just give us a binary deliverable or undeliverable status. Instead, it summarizes the analysis into one of these values:
  • deliverable
  • deliverable_unnecessary_unit
  • deliverable_incorrect_unit
  • deliverable_missing_unit
  • undeliverable
You can switch on these values to update your CRM if it’s helpful for your sales team. Now, back to our application. The last thing left to do is to add this handler to our router at “backend/router.js”.
				
					import { Router } from "express";

import createTemplate from "./template/create.js";
import listTemplates from "./template/list.js";

import createPostcard from "./postcard/create.js";
const router = new Router();

router.post("/templates/create", createTemplate);
router.get("/templates", listTemplates);
router.post("/postcard/create", createPostcard);

export default router;
				
			

Finally, we let our users know that their postcard is on its way to the customer.

Next steps

We’ve set up a form to accept addresses in our app, verified addresses, and converted our bits into atoms. Our application can now trigger physical mail delivery to a customer anywhere in the US. That’s pretty cool!

You can review the project code  before reading this series’s third and final article, where we’ll adjust our application to manage the postcards we’ve sent — including canceling them — and use webhooks to follow our postcard’s journey through the system.

Try Lob’s Print & Mail API for yourself now, or continue to article three to add mail management to our app.

If you’re interested in developing expert technical content that performs, let’s have a conversation today.

Facebook
Twitter
LinkedIn
Reddit
Email

POST INFORMATION

If you work in a tech space and aren’t sure if we cover you, hit the button below to get in touch with us. Tell us a little about your content goals or your project, and we’ll reach back within 2 business days. 

Share via
Copy link
Powered by Social Snap