Search

Mailing a Postcard with JavaScript Part 1: Creating a Postcard with Lob and Node.j

Lob’s Print & Mail and Address Verification APIs enable developers to interact with Lob’s services programmatically. You can mail a postcard or letter to your customers at critical points in their journey as easily as you might send an email. These physical reminders help you keep in touch with clients, and encourage clients to keep purchasing your products and services. 

In this three-part tutorial, we’ll create a postcard template, verify our recipient’s address, send a postcard, and track it along its journey. We’ll create a Javascript application to do all this, so our users can access everything they need in one place.

To follow along, you’ll need your own Lob account. You can sign up here and find your API keys in your settings. Take a note of the secret and the publishable API keys. We’ll use the publishable key any time we interact with Lob from the frontend and the secret key anywhere we access the Lob API from the backend.

Lob’s APIs are well documented, and we can choose from SDKs in various languages. We’ll focus on Node.js in this series, but the overall approach will work in whichever language you choose.

Our template creation app                    

Our app comprises two parts: a Vue frontend and a Node backend. In this part of our tutorial, we’ll enable our users to create postcard templates that they can later use to send physical postcards to their customers.

Our users will create the template with HTML and CSS then store it on the Lob server. This template has the layout and text ready to send to all our user’s customers. Once we submit these templates to Lob, we can use them as many times as we’d like. We could send hundreds — or even thousands — of postcards from a single template.

Let’s start creating our application by giving our users the ability to build and submit their own templates. In this tutorial, we’ll use one of Lob’s example postcard templates and allow our users to change the background picture and text.

Creating the application’s Vue front end

First, let’s instantiate a new Vue application using Vite:

				
					npm init vite@latest
				
			

Let’s name our project and select Vue. We won’t use TypeScript.

We follow the instructions Vite displays on our screen to install the dependencies and get the starter site up and running.

Before we start making our application, create a file called .env to save our environment variables. The Vite framework exposes environment variables that have a “VITE_” prefix. For more information on this, read the Vite documentation. As a developer, you never want to make a commit to Github that contains sensitive login information.

				
					// .env
VITE_CLOUDINARY_NAME=<insert_name>
VITE_CLOUDINARY_PRESET=<insert_preset>
				
			

Now, let’s create a new component for our front template, Front.vue, and add the template and styling based on one of Lob’s examples. Specifically, we look at the front of the Product Promotion postcard. We will replace the default HelloWorld component with the new Front component in the App.vue file.

				
					// src/App.vue
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import FrontVue from './components/Front.vue';
</script>
<template>
<FrontVue />
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
				
			
				
					<template>
<div
class="body"
:style="{ backgroundImage: `url(https://s3.us-west-2.amazonaws.com/public.lob.com/solutions/Lob_demo_postcard_conversion/Retail+psc/4x6+Retail+Postcard/Links/indoor-4148898.jpg)` }"
>
<div class="header">Love the home you live in</div>
<div class="logo">virtonis</div>
<div id="safe-area"></div>
</div>
</template>
<style scoped>
*,
*:before,
*:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
textarea {
font: inherit;
}
@font-face {
font-family: "Lato-Bold";
src: url(https://s3.us-west-2.amazonaws.com/public.lob.com/solutions/Lob_demo_postcard_conversion/Retail+psc/4x6+Retail+Postcard/Document+fonts/Lato-Bold.ttf)
format("truetype");
}
@font-face {
font-family: "Lato-Light";
src: url(https://s3.us-west-2.amazonaws.com/public.lob.com/solutions/Lob_demo_postcard_conversion/Retail+psc/4x6+Retail+Postcard/Document+fonts/Lato-Light.ttf)
format("truetype");
}
@font-face {
font-family: "Lato-Regular";
src: url(https://s3.us-west-2.amazonaws.com/public.lob.com/solutions/Lob_demo_postcard_conversion/Retail+psc/4x6+Retail+Postcard/Document+fonts/Lato-Regular.ttf)
format("truetype");
}
.body {
position: absolute;
top: 0;
left: 0;
width: 6.25in;
height: 4.25in;
margin: 0;
padding: 0;
background-size: 6.25in 4.25in;
background-repeat: no-repeat;
}
.header {
position: absolute;
width: 4.3232in;
height: 1.8625in;
left: 0.9138in;
top: 0.2847in;
font-size: 39.247pt;
line-height: 35.804pt;
font-family: "Lato-Light";
}
.logo {
position: absolute;
/* width: 1.2807in; */
height: 0.4407in;
right: 0.2in;
top: 3.7072in;
font-size: 24.787pt;
line-height: 29.745pt;
font-family: "Lato-Regular";
}
#safe-area {
position: absolute;
width: 5.875in;
height: 3.875in;
left: 0.1875in;
top: 0.1875in;
background-color: rgba(255, 255, 255, 0.5);
}
</style>
				
			

We want to allow our users to change each of these elements. We’ll use the Vue composition API to help us do that.

We add a <script setup> tag to our component and set up some reactive variables. We then set the default values to those the template already uses, so nothing changes on the frontend when we update the template.

				
					// src/components/Front.vue
<script setup>
import { ref } from "vue";
const imgSrc = ref("https://s3.us-west-2.amazonaws.com/public.lob.com/solutions/Lob_demo_postcard_conversion/Retail+psc/4x6+Retail+Postcard/Links/indoor-4148898.jpg");
const headerText = ref("Love the home you live in")
const logoText = ref("virtonis")
</script>
<template>
<div class="body" :style="{ backgroundImage: `url(${imgSrc})` }">
<div class="header">{{ headerText }}</div>
<div class="logo">{{ logoText }}</div>
<div id="safe-area"></div>
</div>
</template>
				
			
Now that we have reactive values, we need to give our users some way to interact with those values. We use the v-model to create a two-way binding between the input and the reactive value for the header and logo text. As we type into these fields, we’ll be able to see the form updating.
				
					// src/components/Front.vue
<template>
<div class="body" :style="{ backgroundImage: `url(${imgSrc})` }">
...
<div class="controls">
<form @submit.prevent>
<h2>Edit your template</h2>
<div class="form-field">
<label for="headerText">Header text:</label>
<textarea id="headerText" v-model="headerText" />
</div>
<div class="form-field">
<label for="logoText">Logo text:</label>
<input id="logoText" v-model="logoText" />
</div>
</form>
</div>
</div>
</template>
<style scoped>
...
.controls {
position: absolute;
top: 4.5in;
}
.form-field {
display: flex;
flex-direction: column;
}
label {
text-align: left;
margin-bottom: 8px;
margin-top: 8px;
}
</style>
				
			

We then upload the image to a third-party service, like Cloudinary. Cloudinary has a helpful library that provides the upload modal, handles the cloud storage, and provides a URL we can pass into our template.

We first need to add the script import for Cloudinary to our main index.html file right above the “main.js” script tag that holds our Vue app:

				
					<script
src="https://widget.cloudinary.com/v2.0/global/all.js"
type="text/javascript"
></script>
				
			

When we instantiate the Cloudinary script, it adds a cloudinary library with an openUploadWidget to our window object.

Let’s create a handler function to open the widget and update our state when the widget completes. To follow along, sign up for Cloudinary to get your cloud name and upload preset. Put these values in the .env file we created earlier.

				
					// src/components/Front.vue
function openUploadModal() {
window.cloudinary.openUploadWidget(
{
cloud_name: import.meta.env.VITE_CLOUDINARY_NAME,
upload_preset: import.meta.env.VITE_CLOUDINARY_PRESET
},
(error, result) => {
if (!error && result && result.event === "success") {
imgSrc.value = result.info.url
}
}).open();
}
				
			

Next, we add a button to our template that will trigger this widget when the user clicks.

				
					// src/components/Front.vue
<form @submit.prevent>
&hellip;
<div class="form-field">
<label for="backgroundImage">Background Image:</label>
<button @click="openUploadModal()">Upload files</button>
</div>
</form>
				
			

The next tasks we need to tackle is to bring some routing to our Vue app. After saving the postcard template, we want the app to redirect to another page that will list all of the templates that we have saved.  

Add the vue-router package to our project by running the following command: “npm install –save vue-router@4”. Create a new file under src/router/index.js and add the following:

				
					// src/router/index.js
import { createWebHistory, createRouter } from "vue-router";
import ListTemplates from "../components/ListTemplates.vue";
import Front from "../components/Front.vue";
const routes = [
{ path: "/", component: Front, name: "Home" },
{ path: "/list", component: ListTemplates, name: "ListTemplates" },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
				
			

Since we are missing the ListTemplates component, let’s create a stub of this for this time being.

				
					// src/components/ListTemplates.vue
<script setup>
import { reactive, onMounted } from "vue";
const templates = reactive({});
onMounted(() => {
fetch("http://localhost:3030/templates")
.then((data) => data.json())
.then((data) => (templates.value = data));
});
</script>
<template>
<p>We will show the list of templates here</p>
</template>
				
			

The last step we need to do is put a reference to the router in the main.js file and update the App.vue component to use the router.

				
					// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from "./router";
createApp(App).use(router).mount('#app')
				
			
				
					// src/App.vue
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
</script>
<template>
<router-view></router-view>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
				
			

Now, we add our template name. Then, we send the template information to our backend.

				
					// src/components/Front.vue
<script setup>
import router from "../router";
...
const templateName = ref("")
async function submitTemplate() {
await fetch(`http://localhost:3030/templates/create?logoText=${logoText.value}&templateName=${templateName.value}&backgroundImage=${imgSrc.value}&description=${headerText.value}`, { method: "POST" })
router.push("/list")
}
</script>
<template>
...
<div>
<h2>Finished with your postcard design?</h2>
<div class="form-field">
<label for="templateName">Give it a name</label>
<input id="templateName" v-model="templateName" />
</div>
<button @click="submitTemplate()">Upload template</button>
</div>
</template>
				
			

Let’s next hop over to the backend and get our route ready to receive this information.                

Creating the application’s Node backend

To create our back end, we will create a new folder called “backend.” After changing into this directory, we will create a package.json file with the following contents:

				
					// package.json
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"node-fetch": "^3.0.0",
"nodemon": "^2.0.13"
}
}
				
			

This package.json file lists our dependencies —  express web framework to structure our app; dotenv to keep our environment variables secret; cors to handle data sent to our front end; and nodemon to restart our server every time we save. We added “type“: “module” to our package.json to use esm import and export. Run the command “npm install” to install all of our dependencies.

Let’s next create an index.js file and add a basic web server setup. We also make and import router.js to organize our routes.

				
					// index.js
import express from "express";
import cors from "cors";
import router from "./router.js";
import dotenv from "dotenv";
dotenv.config();
const app = express();
const port = process.env.PORT || 3030;
app.use(cors());
app.use("/", router);
app.listen(port, () => {
console.log(`Listening on port ${port}.`);
});
				
			
				
					// router.js
import { Router } from "express";
import createTemplate from "./template/create.js";
const router = new Router();
router.post("/templates/create", createTemplate);
export default router;
				
			

After we set this up, we will need to create a .env file that will hold our Lob API test key.

				
					// .env
LOB_SECRET_API_KEY=<place your key here>
				
			

We’ll be sending the variables as query parameters from Vue to our backend. We have the replicated template on the back end and populate it with the user’s front-end data.

We’re effectively adding dynamic values to a large template literal. We’ll use node-fetch, a Node implementation of the browser fetch API, to send our data to Lob. We need to encode the data and identify ourselves with the API correctly. Let’s modify the createTemplateHandler function to add the call to Lob API.
				
					// template/create.js
import fetch from "node-fetch";
export default async function createTemplateHandler(req, res) {
try {
const htmlString = createHTMLTemplate(req.query);
const params = new URLSearchParams();
const templateName = req.query.templateName || new Date().toString();
params.append("description", templateName);
params.append("html", htmlString);
await fetch("https://api.lob.com/v1/templates", {
method: "POST",
body: params,
headers: {
Authorization: `Basic ${Buffer.from(
process.env.LOB_SECRET_API_KEY + ":"
).toString("base64")}`,
},
});
res.send({ message: "Template created." });
} catch (error) {
res.send(error);
}
}
				
			
To keep the third-party packages to a minimum, we use Node’s UrlSearchParams rather than a package such as form-data. UrlSearchParams also sets the headers we need automatically. We append our description and HTML parameters to the data we send to Lob, then prepare our headers. We use a basic username and password to authenticate ourselves with the Lob API. The username should be our API key, which we get from the environment variable LOB_SECRET_API_KEY, and the password should be blank. This configuration is the same as setting an Authorization header, as the code above shows. Once our authentication is successful, we send a message back to our Vue application to let it know we’re done.
				
					res.send({ message: "Template created." });
				
			
Now that we’ve created the template, we make a route to list our templates and consume the route on the frontend. In Node, we use a straightforward GET request using node-fetch:
				
					// templates/list.js
import fetch from "node-fetch";
export default async function listTemplate(req, res) {
const response = await fetch("https://api.lob.com/v1/templates", {
headers: {
Authorization: `Basic ${Buffer.from(
process.env.LOB_SECRET_API_KEY + ":"
).toString("base64")}`,
},
});
const templates = await response.json();
res.send(templates.data);
} 
				
			

Now that we have the functionality to grab our saved templates from the Lob API, let’s add that endpoint to the Express app.

				
					// router.js
import { Router } from "express";
import createTemplate from "./template/create.js";
import listTemplate from "./template/list.js";
const router = new Router();
router.post("/templates/create", createTemplate);
router.get("/templates", listTemplate);
export default router;
				
			
We authenticate ourselves in the same way, then pass that data on to our clients. We want to get that data in Vue and display it to our users. We fetch and process the data using the onMounted function. We then update our reactive value, which triggers our template to rerender. So let’s update the ListTemplates component that we stubbed out earlier.
				
					// src/components/ListTemplates.vue
<script setup>
import { reactive, onMounted } from "vue";
const templates = reactive({});
onMounted(() => {
fetch("http://localhost:3030/templates")
.then((data) => data.json())
.then((data) => (templates.value = data));
});
</script>
<template>
<ul class="flex">
<li v-for="template in templates.value">
<h3>{{ template.description }}</h3>
<p>Created on {{ template.date_created }}.</p>
<p><a :href="'https://dashboard.lob.com/#/templates/' + template.id" target="_blank">Preview</a></p>
</li>
</ul>
</template>
<style scoped>
li {
display: block;
}
.template-frame {
width: 200px;
height:300px;
overflow: hidden;
}
</style>
				
			

Using the v-for directive, we iterate over the templates and destructure the more relevant values.

Next steps

In this part of the tutorial, we’ve built our application to enable users to create and view templates in Lob. We have the project code saved here for you to review as you carry on to the next part of this tutorial. Next time, we’ll use these templates to send our real-life postcards, changing bits to atoms.

A well-designed postcard can enhance the relationship between your customers and your brand. Try Lob’s Print & Mail API yourself now, or continue to the second part of this tutorial to learn how to verify an address before sending a postcard.

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