Building a Wedding Invite System with Node.js, Vonage, and Directus
Tags: directus, node, express, vonage
Published: 2023-08-14 07:08:07

---
title: Building a Wedding Invite System with Node.js, Vonage, and Directus
published: true
description: Learn how to build an events invitation system with a multiple ticket types.
tags: directus, nodejs, express, vonage
cover_image: "https://marketing.directus.app/assets/4b3f2aef-d945-4945-9e99-2979ae64c147.png"
canonical_url: "https://docs.directus.io/blog/wedding-invite-vonage"
---

My wedding is coming up in just over a week, which is exciting. But you know what isn't exciting? Getting a confirmed guest list with all of the variations in invites and information gathered consistently. I'm going to show you how I built my wedding invite system with various types of invite (all-day/ceremony-only), auto-formatted contact information when RSVPs were submitted, and generated a constantly-updated guest-list. 

## Before You Start

You will need Node.js and a text editor installed on your computer. 

You will also need a Directus project running - either using [Directus Cloud](https://directus.cloud) or by Self Hosting - and an API token for your admin user. 

Finally, sign up for a [Vonage Developer account](https://developer.vonage.com/sign-up) and take note of your API Key and Secret.

In your Directus project, create the following collections (and fields):
- people - Primary Key Field: id (Generated UUID)
	- first_name (Type: String, Interface: Input)
	- last_name (Type: String, Interface: Input)
	- rsvp (Type: Boolean, Interface: Toggle)
	- requirements (Type: String, Interface: Input)
	- is_plus_one (Type: Boolean, Interface: Toggle)
- invites - Primary Key Field: slug (Manually entered string)
	- message (Type: String, Interface: Input)
    - people (Type: Alias, Interface: One to Many)
		- Related Collection: people
		- Foreign Key: invite
		- Display Template `First Name Last Name` 
	- plus_one_allowed (Type: Boolean, Interface: Toggle)
	- ceremony_invite (Type: Boolean, Interface: Toggle)
	- completed (Type: DateTime, Interface: Datetime)
	- email (Type: String, Interface: Input)
	- phone (Type: String, Interface: Input)

One invite will have multiple people, and optional characteristics (allowed +1, invited to ceremony). When guests respond, they will provide requirements (dietary/accessibility), and the person filling it in will provide an email and phone number. Finally, if given a +1 and it's used, a new person will be created and `is_plus_one` will be set to `true`. 

Here's an illustration of an invite page, annotated to show where values in Directus will alter it.

![A screenshot shows a mockup of the interface. A custom message is shown on each invite, a section with ceremony information is only shown if they are invited to that portion. A yes/no toggle is shown for each guest, and a +1 name and requirements section is only shown if the invite grants a +1](https://marketing.directus.app/assets/e6c7eabd-16f3-4cf3-9fed-b843476af69e)

The most minimal invite (one guest, reception-only, no +1) looks like this:

![A mockup showing a basic invite with one guest, no ceremony or +1 information or form](https://marketing.directus.app/assets/d8c521b7-4331-44a1-98e7-e72ce09b6fa4)

Create a new directory for this project and open it in a code editor. Create a `.env` file and populate it with your keys:

```
DIRECTUS_URL=your_project_url_here
DIRECTUS_TOKEN=your_admin_token_here
```

Run `npx gitignore node` to create a suitable `.gitignore` file for this project. Create a `package.json` file with `npm init -`y and then install our dependencies:

```
npm install dotenv express hbs @directus/sdk
```

If you don't want to follow along, you can now skip to the bottom of this tutorial for the complete code.

Create an `index.js` file and open it in your code editor.

## Set Up Application

Create and set up an Express.js application with the [handlebars](https://handlebarsjs.com/) view engine:

```js
import 'dotenv/config';
import express from 'express';
import { engine } from 'express-handlebars';

const app = express();
app.use(express.static('public'));
app.use(express.urlencoded({ extended: true }));

app.engine('handlebars', engine());
app.set('view engine', 'handlebars');
app.set('views', './views');

app.get('/invite/:slug', (req, res) => {
  res.render('invite', { 
    layout: false, 
    name: req.params.slug 
  });
});

app.listen(3000);
```

Create a `views` directory and, inside of it, an `invite.handlebars` file: 

```html
<!DOCTYPE html>
<html>
<body>
   {{ name }}
</body>
</html>
```

Run your application with `node index.js` in the terminal, and navigate to localhost:3000/invite/hello. You should see a page with 'hello'. 

## Set Up Directus SDK

Import and initialize a new Directus client with rest features. Import the `createItem`, `readItem`, and `updateItem` functions for later.

```js
import 'dotenv/config';
import express from 'express';
import { engine } from 'express-handlebars';

import { createDirectus, staticToken, rest, createItem, readItem, updateItem } from '@directus/sdk'; // [!code ++]
const directus = createDirectus(process.env.DIRECTUS_URL) // [!code ++]
  .with(rest()) // [!code ++]
  .with(staticToken(process.env.DIRECTUS_TOKEN)); // [!code ++]
```

## Create Invite Pages

Update the invite route handler to utilize the Directus SDK and read the invite from your Directus project. 

```js
app.get('/invite/:slug', async (req, res) => {
  const data = await directus.request( // [!code ++]
    readItem('invites', req.params.slug, { // [!code ++]
      fields: ['*', { people: [ 'id', 'first_name', 'last_name' ] }] // [!code ++]
    }) // [!code ++]
  ); // [!code ++]

  res.render('invite', { 
    layout: false, 
    name: req.params.slug // [!code --]
    ...data // [!code ++]
  }); 
});
```

Update the content of the `<body>` tag to include values from the invite:

```html
<body>
  <h1>You're invited!</h1>
  <p>{{ message }}</p>
  <div>
    {{#if ceremony_invite}}
      <p>Ceremony-specific information</p>
    {{/if}}
    <p>Reception-specific information</p>
  </div>
</body>
```

Show everyone the personalized message as part of their invite and the reception details. Only a subset of guests are invited to the ceremony as denoted by `cermeony_invite` being set to `true`. 

Create five items in the `people` collection with just a name, and four `invites` to test: 
1. `ceremony_invite` = `false`, `plus_one_allowed` = `false`, `slug` = 'a'
2. `ceremony_invite` = `true`, `plus_one_allowed` = `false`, `slug` = 'b'
3. `ceremony_invite` = `false`, `plus_one_allowed` = `true`, `slug` = 'c'
4. `ceremony_invite` = `true`, `plus_one_allowed` = `true`, `slug` = 'd' (assign two people to this invite)

This should give all key permutations to test against. Restart your server, navigate to localhost:3000/invite/a and you should see only the reception information. When you go to localhost:3000/invite/b you should see ceremony information as well.

### Build RSVP Form

Underneath the existing `<div>`, add a conditional that will only show the form is the invite has not yet been RSVP'd to: 

```html
{{#if completed}}
  <p>Thanks for responding</p>
{{else}}
  <!-- Further code goes here -->
{{/if}}
```

In the <span v-pre>`{{else}}`</span> block, create a form:

```html
<form action="/rsvp" method="POST">

  {{#each people}}
    <div style="border: 1px solid black;">
      <h2>{{ this.first_name }} {{ this.last_name }}</h2>

      <input type="radio" name="rsvp-{{ this.id }}" value="false" id="rsvp-no-{{ this.id }}" required>
      <label for="rsvp-no-{{ this.id }}">Accept</label><br>
      
      <input type="radio" name="rsvp-{{ this.id }}" value="true" id="rsvp-yes-{{ this.id }}" required>
      <label for="rsvp-yes-{{ this.id }}">Regrets</label><br>

      <input type="text" name="requirements-{{ this.id }}" id="requirements-{{ this.id }}" placeholder="Requirements">
    </div><br>
  {{/each}}

  <p>Only one needed per invite</p>
  <input type="email" placeholder="Email" name="email" required>
  <input type="text" placeholder="Phone" name="phone" required>
  <input type="hidden" value="{{ slug }}" name="slug">
  <input type="submit">
</form>
```

Let's break this down:
1. The form will submit a POST request to `/rsvp`. This will be created later.
2. For every person assigned to this invite, create a set of three inputs - a pair of radio buttons for RSVP yes/no, and one to optionally collect any accessibility or dietary requirements. Note that all inputs have a dynamic name which includes that person's ID from the database. For example, <span v-pre>`name="rsvp-{{ this.id }}"`</span> would become `name="rsvp-ff028423-7111…"`
3. There is one email and phone number collected per RSVP.
4. A hidden field contains the slug, so it is also submitted with the rest of the form.

### Add +1 Form Fields

If the invite has `plus_one_allowed` set to `true`, show additional fields to collect their information:

```html
{{/each}}

{{#if plus_one_given}} // [!code ++]
  <div style="border: 1px solid black;"> // [!code ++]
    <h2>Your +1</h2> // [!code ++]
    <input type="text" name="first-name-plus-one" id="first-name-plus-one" placeholder="First Name"> // [!code ++]
    <input type="text" name="last-name-plus-one" id="last-name-plus-one" placeholder="Last Name"> // [!code ++]
    <input type="text" name="requirements-plus-one" id="requirements-plus-one" placeholder="Requirements"> // [!code ++]
  </div> // [!code ++]
{{/if}} // [!code ++]

<p>Only one needed per invite</p>
```

Restart your server, navigate to localhost:3000/invite/b and you should not see the +1 fields. When you go to localhost:3000/invite/d you should. Here's what the form looks like when a +1 is allowed on the invite:

![A screenshot shows the full invite page with no styling](https://marketing.directus.app/assets/43197611-de6a-4d70-8f20-595314fbb197)

It isn't winning any design awards, but it works. 

## Submit RSVP

Create a new route handler in `index.js` for the POST submission:

```js
app.post('/rsvp', async (req, res) => {
  console.log(req.body);
});
```

Restart the server, fill and submit the form. The terminal should look something like this:

![A terminal shows a logged object with 7 properties. 2 of them start rsvp and then a UUID. 2 of them start requirements and end with the same UUIDs.](https://marketing.directus.app/assets/44f58109-dca3-4133-8b12-9e4ef2a6ef5a)

### Update People With Their RSVP

To get a list of only people IDs, filter this object to an array of just the properties starting with `rsvp`, and then remove `rsvp-` from the start of the strings. Under the `console.log`:

```js
const rsvps = Object.keys(req.body)
  .filter(k => k.includes('rsvp'))
  .map(k => k.split('rsvp-')[1]);

Then, loop through these IDs and update each person with their RSVP and requirements:

for(let rsvp of rsvps) {
  await directus.request(
    updateItem('people', rsvp, { 
      rsvp: req.body[`rsvp-${rsvp}`],
      requirements: req.body[`requirements-${rsvp}`]
    })
  );
}
```

### Create Person Item For +1

If the +1 form was filled in, create a new person and add their ID to the RSVPs list. Under the `for` loop add the following:

```js
if(req.body['first-name-plus-one']) {
  const { id } = await directus.request(
  createItem('people', {
    first_name: req.body['first-name-plus-one'],
    last_name: req.body['last-name-plus-one'],
    requirements: req.body['requirements-plus-one'],
    is_plus_one: true,
    rsvp: true
   })
  );
  rsvps.push(id)
}
```

### Update Invite

Each invite contains a phone number and email address of the lead contact. Under the `for` loop, update the invite:

```js
await directus.request(
  updateItem('invites', req.body.slug, { 
    email: req.body.email,
    phone: req.body.phone,
    people: rsvps,
    completed: new Date()
  })
);
```

If there was a +1, the new person will be added to the invite. The value of `completed` will also be set to the current Datetime. In Directus, you'll easily be able to see when an RSVP was made. In the application, this will replace the form with the message "Thanks for responding".

As the form will not appear when `completed` has a value, redirect back to the same page and it will appear as if the form has been replaced with the thank you message:

```js
res.redirect(`/invite/${req.body.slug}`);
```

## Standardize Phone Numbers

I can tell you from past experience - no one ever fills in their phone number consistently, and I wanted to build a batch update system so we could send people important messages as the day approaches. 

To format numbers, I used the Vonage Number Insight API. In your Directus Project Settings create a new Flow. 

Use an non-blocking Event Hook Trigger so the item is added to the collection and then the flow is run. Keep the scope to only the `items.update` on the `Invites` collection.

Add a new Request URL operation and set the URL to <span v-pre>`https://api.nexmo.com/ni/basic/json?api_key=VONAGE_API_KEY&api_secret=VONAGE_API_SECRET&number={{$trigger.payload.phone}}`</span>, being sure to change the values for your key and secret. 

Once the request has been successful, add an Update Data operation on the Invites collection. Set the payload to the following: 

```js
{
    "phone": "{{$last.data.international_format_number}}"
}
```

Save the Flow and test it by submitting a new invite RSVP. You may need to manually set an invite's `Completed` value to `null` to see the form again. The whole flow should look like this:

![A flow has one trigger and two operations. The trigger is an actio nevent hook triggered when an item in the invites collection is updated. The first operation is a Request URL to the Vonage API. The final operation updates the invites item.](https://marketing.directus.app/assets/3c8126e4-f4e9-47e6-a2a8-6e20dbc98e47)

## See Guest List

To see a running guest-list, navigate to the People collection in the content module and add a filter `RSVP = true`. You can see just the ceremony list by adding the filter `Invite -> Ceremony Invite = true`.

## Summary & Next Steps

In this tutorial, you have created a wedding invite and RSVP system backed by a Directus database, and then standardized provided data using the Vonage Number Insight API.

Now that you have standardized data, you may choose to send batch messages via SMS or email using Flows. 

If you have any questions or feedback, feel free to join our [Discord server](https://directus.chat).

## Complete Code

`index.js`

```js
import 'dotenv/config';
import express from 'express';
import { engine } from 'express-handlebars';
import { createDirectus, staticToken, rest, createItem, readItem, updateItem } from '@directus/sdk';
const directus = createDirectus(process.env.DIRECTUS_URL)
  .with(rest())
  .with(staticToken(process.env.DIRECTUS_TOKEN));

const app = express();
app.use(express.static('public'));
app.use(express.urlencoded({ extended: true }));

app.engine('handlebars', engine());
app.set('view engine', 'handlebars');
app.set('views', './views');

app.get('/invite/:slug', async (req, res) => {
  const data = await directus.request(
    readItem('invites', req.params.slug, { 
      fields: ['*', { people: [ 'id', 'first_name', 'last_name' ] }] 
    })
  );
  res.render('invite', { layout: false, ...data });
});

app.post('/rsvp', async (req, res) => {
  console.log(req.body)
  // Get list of RSVP IDs
  const rsvps = Object.keys(req.body)
    .filter(k => k.includes('rsvp'))
    .map(k => k.split('rsvp-')[1]);

  // Update people with RSVPs
  for(let rsvp of rsvps) {
    await directus.request(
      updateItem('people', rsvp, { 
        rsvp: req.body[`rsvp-${rsvp}`],
        requirements: req.body[`requirements-${rsvp}`]
      })
    );
  }

  // Add +1 if exists
  if(req.body['first-name-plus-one']) {
    const { id } = await directus.request(
      createItem('people', {
        first_name: req.body['first-name-plus-one'],
        last_name: req.body['last-name-plus-one'],
        requirements: req.body['requirements-plus-one'],
        is_plus_one: true,
        rsvp: true
      })
    );
    rsvps.push(id);
  }

  // Update invite
  await directus.request(
    updateItem('invites', req.body.slug, { 
      email: req.body.email,
      phone: req.body.phone,
      people: rsvps,
      completed: new Date()
    })
  );

  res.redirect(`/invite/${req.body.slug}`);
});

app.listen(3000);
```

`views/invite.handlebars`

```html
<!DOCTYPE html>
<html>
<body>
  <h1>You're invited!</h1>
  <p>{{ message }}</p>
  <div>
    {{#if ceremony_invite}}
      <p>Ceremony-specific information</p>
    {{/if}}
    <p>Reception-specific information</p>
  </div>
  {{#if completed}}
    <p>Thanks for responding</p>
  {{else}}
    <form action="/rsvp" method="POST">
      {{#each people}}
        <div style="border: 1px solid black;">
          <h2>{{ this.first_name }} {{ this.last_name }}</h2>

          <input type="radio" name="rsvp-{{ this.id }}" value="false" id="rsvp-no-{{ this.id }}" required>
          <label for="rsvp-no-{{ this.id }}">Accept</label><br>
          
          <input type="radio" name="rsvp-{{ this.id }}" value="true" id="rsvp-yes-{{ this.id }}" required>
          <label for="rsvp-yes-{{ this.id }}">Regrets</label><br>

          <input type="text" name="requirements-{{ this.id }}" id="requirements-{{ this.id }}" placeholder="Requirements">
        </div>
        <br>
      {{/each}}
      {{#if plus_one_allowed}}
      <div style="border: 1px solid black;">
        <h2>Your +1</h2>
        <input type="text" name="first-name-plus-one" id="first-name-plus-one" placeholder="First Name">
        <input type="text" name="last-name-plus-one" id="last-name-plus-one" placeholder="Last Name">
        <input type="text" name="requirements-plus-one" id="requirements-plus-one" placeholder="Requirements">
      </div>
      {{/if}}
      <p>Only one needed per invite</p>
      <input type="email" placeholder="Email" name="email" required>
      <input type="text" placeholder="Phone" name="phone" required>
      <input type="hidden" value="{{ slug }}" name="slug">
      <input type="submit">
    </form>
  {{/if}}
</body>
</html>
```}