Guides & Tutorials

Building an Event Registration Page with Composition API and Serverless Functions

composition api and serverless functions

The Composition API was born out of the desire to offer Vue developers a different way of writing and organizing code while building Vue apps. It provided some flexibilities around how we build Vue applications that weren’t possible in the Options API, but like every other methodology, they all have trade-offs.

The Composition API at a high-level overview offers developers a different way of writing code that ensures that codebases remain legible, readable and easier to maintain even as it grows larger.

In this post, we’ll look at how to collect data from a conference registration form with the Composition API and post it to a serverless function that will receive the request and return a response. You can do pretty much anything else you want in the function, like saving users to a database using Hasura, or handling authentications with Netlify Identity.

If you’ll like to code along, I’ve prepared a starter repository here for you. Check out the start branch and build from there.

For visual context, this is what the form looks like at the end.

event registration form

You should already have a src/components/Registration file set up for you in the start branch so we’ll go ahead and walkthrough handling the form fields with the Composition API. We’ll have fields for name, email, track, mobile track and tickets. This means that we need a way to handle state for these fields.

Reactivity in the Composition API

In the Options API, all the pieces of data in the data() option is tracked and made reactive in Vue by default. But, in the Composition API, all the data is static by default. This means that we need a way to make it reactive. The Composition API comes with the reactive and ref helper methods which are available by default in Vue 3. Wrapping the data we need in our Vue app with these methods ensures that they remain reactive across the app.

ref

ref provides us a way to make data reactive. For instance, if I wanted to track when a form is submitted, I could create a ref variable for it like so:

<script>
  import { ref } from 'vue'
  export default {
    setup() {
      const isUserRegistered = ref(false)
      return {
        isUserRegistered
      }
    }
  }
</script>

By returning isUserRegistered in the setup() function, I have exposed it to the template.

Accessing ref values

To access the value of ref variables, you have to append .value to it. Consider this example:

<script>
  import { ref } from 'vue'
  export default {
    setup() {
      const isUserRegistered = ref(false)
      const sayHi = () => {
        if (!isUserRegistered.value){
          // say hi to the registered user :
        }
      }
      return {
        isUserRegistered,
        sayHi
      }
    }
  }
</script>

This might seem like you need to write a lot of .value‘s when using ref but that is not the case in the render context. When you return a ref in the setup() function, it automatically unwraps to the inner value and becomes accessible to the template. As a result, you won’t need to append .value to access a ref in the template:

<template>
  <div v-if="!isUserRegistered">
    <!-- more template thingy -->
  </div>
</template>

Reactive

Just like ref, reactive also provides us with a way of keeping data reactive in Vue 3. Albeit different from ref, it achieves the same result. Let’s demonstrate how the reactive helper method works in the registration form:

<script>
  import { reactive } from 'vue'
  export default {
    setup() {
      const formData = reactive({
        name: "",
        email: "",
      })
      return {
        formData
      }
    }
  }
</script>

formData in the setup() function above is a reactive object that contains all the pieces of data supplied by the user. We return it in the setup() function to expose it for use in the template. Speaking about the template, this is how we’ll use it there:

<template>
  <div>
    <h1>Let's JAM in Space!</h1>
    <p>
      Register for the upcoming Jamstack conference in space! <br />
      <strong>Free </strong>for one and <strong>$5 </strong> for 2+ tickets
    </p>
    <form>
      <label for="name">Name</label>
      <input id="name" v-model="formData.name" placeholder="Enter your name" />
      <label for="email">Email </label>
      <input
        id="email"
        v-model="formData.email"
        placeholder="Enter your email"
      />
    </form>
  </div>
</template>

You can immediately see why you may not want to keep binding data to the template using formData as doing that could lead to a bloated markup when you have multiple properties inside the object. In my case, I decided to destructure formData and get name and email directly from it like so:

return {
  ...formData // spread
};
//OR
const { name, email } = formData; // destructure
return {
  name,
  email
};

If we do that, then you might assume that name and email would be directly bound to the template so:

<template>
  <div>
    <form>
      <input v-model="name" placeholder="Enter your name" />
      <input v-model="email" placeholder="Enter your email" />
    </form>
  </div>
</template>

But unfortunately, that’s not possible for one reason: spreading or destructuring the formData object will remove reactivity from the different pieces of data inside the object. It suffices to say that it seems the reactive object cannot be destructured or spread directly with the spread operator. What do we do then?

toRefs

With respect to the snippet above, toRefs is a helper method that allows us to achieve the spread effect on the formData object while retaining reactivity for the different pieces of data inside it. In our case, we can use it to wrap the formData object like so:

<script>
  import { reactive, toRefs } from 'vue'
  export default {
    setup() {
      const formData = reactive({
        name: "",
        email: "",
        // other piece of data
      })
      return {
        ...toRefs(formData)
      }
    }
  }
</script>

With this, we can go ahead and use name and email directly in the template like we wanted to do here.

Computed

Computed properties in the Composition API work the same way they do in the Options API. However, they have a slightly different syntax. Imagine that I have an imaginary coupon that gives users with 2 or more tickets a $3 discount from the total price of their ticket. If that was the case, then I would use a computed property to calculate the total price of the ticket like this:

<script>
  import { computed, reactive, toRefs } from 'vue'
  export default {
    setup() {
      const formData = reactive({
        price: 0,
        tickets: 0,
        coupon: 3,
        discountedPrice: computed(() => {
          if (formData.tickets > 1) {
            return formData.price - formData.coupon;
          } else {
            return 0
          }
        });
        // other pieces of data
      })
      return {
        ...toRefs(formData),
      }
    }
  }
</script>

discountedPrice in the snippet above is a computed property that tracks all of it’s dependencies (formData.price and formData.coupon).

Here’s an even more interesting scenario. Let’s introduce a tickets property to the formData object, and calculate price based on how many tickets are selected. 1 ticket = $0 (free) while 2 or more tickets will be a flat rate of $5. Computed properties shines in this regard as you can see below:

<script>
  import { computed, reactive, toRefs } from 'vue'
  export default {
    setup() {
      const formData = reactive({
        tickets: 0,
        coupon: 3,
        price: computed(() => {
          if (formData.tickets > 1) {
            return 5 - formData.coupon
          } else {
            return 0
          }
        })
      })
      return {
        ...toRefs(formData)
      }
    }
  }
</script>

Here, the computed property price is tracking both coupon and tickets and ensures that anytime any of the values change, it will update automatically to capture the new changes. Bonus Point: You don’t even need to have a separate discountedPrice property anymore.

Methods

Methods in the Composition API are just regular functions defined in setup(). As the last step to creating and using them, you have to always return it in setup() so that it is exposed to the template. In our case, I want to create a method that takes all the information provided in the form and post it to a serverless function (which doesn’t exist yet). Here’s how to handle it in the Composition API:

<script>
import { reactive, ref, toRefs } from 'vue'
export default {
  setup() {
    const formData = reactive({
      name: "",
      email: "",
      tickets: 0,
      price: 0
    })
    const isUserRegistered = ref(false)
    // method
    const registerUser = () => {
      // "/.netlify/functions/register" is the path to my serverless function
      fetch("/.netlify/functions/register", {
        method:"POST",
        body:JSON.stringify(formData)
      })
        .then((response) => response.json())
        .then(body => {
          // do something with the response
          isUserRegistered.value = true
        })
    }
    return {
      ...toRefs(formData),
      isUserRegistered,
      registerUser
      }
    }
  }
</script>
<template>
  <button @click.prevent="registerUser" type="submit">Register</button>
</template>

Remember, methods in the Composition API are normal JavaScript functions. No special syntax needed. The notable thing here is that just like everything else you need in the template, you have to return it in the setup() function.

Serverless function

Serverless functions make it possible for developers to run a server without the overhead that come with fully managing a server. More-so, it comes with fewer cost implications as it only executes on demand. If this tutorial is your first look at serverless functions, I would recommend this awesome intro to serverless functions course by Jason Lengstorf.

A general rule of thumb for creating serverless functions is that it exports a function called handler and returns a response. The response must be an object containing at least a statusCode that matches a valid HTTP response code and a body that is a string by default. At the barest minimum, this is a valid serverless function:

exports.handler = async (event, ctx) => {
  return {
    statusCode: 200,
    body: "Hello World"
  };
};

event

The event argument in the function above is an object that contains all the information you need to know about the request. Here’s a quick look at the structure:

{
  path: '/.netlify/functions/register',
  httpMethod: 'POST',
  queryStringParameters: {},
  multiValueQueryStringParameters: {},
  headers: {},
  multiValueHeaders: {}
  body: {},
  isBase64Encoded: false
}

This can be very useful to perform all sorts of operations in the function. Like validating the request methods, getting request query parameters, using data from the request body etc. When working with serverless functions, the event argument is your single source of truth for information about the request.

context

The second argument is context. It is an object that provides broader information about the request. In it, you will find information like:

  • The name of the function that was called
  • If Netlify identity is present
  • What the client context is and
  • Lots of other information that you will rarely need, but provided for you regardless.

I’ll go ahead and link you up with this doc that explains it in even more detail.

Now that we know what a serverless function looks like, let’s make one. Create a functions/register.js file in the root of your project and update it with this snippet:

// export a function called handler
exports.handler = async (event, ctx) => {
  // receive the request event and get the user's details from it
  const { name, email, track, mobileTrack, price } = JSON.parse(event.body);
  // Things you can do here!
  // create a user object and do what you want with, like:
  // Save user to a database
  // Authenticate user
  // Send event details to user's email
  // etc.
  const newUserDetails = {
    username: name,
    email: email,
    track: track,
    mobileTrack: mobileTrack,
    price: price
  };
  // Send response back to the client
  return {
    statusCode: 200,
    body: JSON.stringify(newUserDetails)
  };
};

Serverless functions help us perform server-side operations without maintaining a dedicated server. That is what we can do here. The form data sent from the client can be handled however you want in the function. You can save to your database, do server-side validation, handle authentication, roles etc. Talk about dynamic Jamstack? serverless functions is the way to go!

Finally, you’ll need to create a netlify.toml file where you tell Netlify where your functions folder is. This helps Netlify locate your functions and deploy them for you along with the rest of your site.

[build]
functions = "functions"

This site is hosted here on Netlify and you can play around with the repo yourself here on Github. I tried and failed to keep this post under 2k words, but I do hope you found some useful information here on both the Composition API and serverless functions.

More resources

Keep reading

Recent posts

How do the best dev and marketing teams work together?