Going Serverless with Nuxt (Composition API) + AWS Amplify + TypeScript

Going Serverless with Nuxt (Composition API) + AWS Amplify + TypeScript

Introduction

In this article we will be learning how we can integrate AWS Amplify with-in a Nuxt app. We will be going over from setting up an AWS account (but not covered, will give you a link instead) and until we can create and fetch all Todos. The app that we will be building for this tutorial is just a basic Todo app, since the scope of this tutorial is to teach you how you will be able to use AWS Amplify with Nuxt.

I prefer to use TypeScript in this article, but you can still use just plain JavaScript whichever you prefer still results to same Todo app we will be building. But before we continue, let us understand what is Serverless? What does it actually mean?

Serverless does not mean there are no servers. There are still servers that run on the cloud but we don't manage them, meaning we don't have to deal with deploying web servers using Nginx or Apache2, we don't have to provision AWS EC2 instances or GCP Compute Engine instances, we only have to worry about directly to the business logic of the application. Isn't that cool?

For more explanation I will quote a definition from Cloudflare:

Serverless computing is a method of providing backend services on an as-used basis. A serverless provider allows users to write and deploy code without the hassle of worrying about the underlying infrastructure. A company that gets backend services from a serverless vendor is charged based on their computation and do not have to reserve and pay for a fixed amount of bandwidth or number of servers, as the service is auto-scaling. Note that despite the name serverless, physical servers are still used but developers do not need to be aware of them.

Taken from source

Setting up an AWS Account

You can skip this step if you already have setup an AWS account. Otherwise go to this URL aws.amazon.com/console and proceed to creating your AWS account! And click on the button that says "Create a Free Account". You will need a credit card to setup your AWS account otherwise you can't create one.

image.png

I am not going to go over how to setup an AWS account in here since it is an out of scope for this tutorial. But I will give you some links for my recommendation.

Also make sure to create an IAM user since AWS best practices for security is to not use the root user, instead create an IAM user and give it admin privileges and this is the only account you will be using from now on. You will only use the root user to create an admin user for your AWS account.

Installing AWS Amplify CLI

Once you are done with setting up your AWS account, let us then proceed to installing the AWS Amplify CLI! We will be using NPM and install to globally on your system, don't forget to add sudo otherwise you will experience permissions error.

$ npm install -g @aws-amplify/cli

with sudo,

$ sudo npm install -g @aws-amplify/cli

Just to make everything clear, just in case.

Once you are done installing aws-amplify globally on your machine, let's then proceed to setting up configuration details so you can integrate this with-in your AWS account as it will ask you to sign in to AWS Console.

Further steps should be continued from the official docs as they provide you the complete steps in setting up your account.

Installing Nuxt

Open up your terminal and navigate into your projects directory or any directory that you prefer to install the Nuxt app. For me I prefer it to be placed under "tutorials" directory since I am creating a tutorial on how to setup Nuxt with AWS Amplify.

$ cd ~/tutorials

Then proceed to execute the command to start the installation process.

# Where <project_name> is the name of the Nuxt project
# In this case I will name it as "nuxt-aws-amplify-todo-app"

$ npx create-nuxt-app <project_name>

In selecting the installation options, for the programming language I select TypeScript since it is my preferred choice and my "go-to" language. Package manager would just be NPM. And the UI library I select "Vuetify.js" I prefer this one since it provides a lot of Material design components out of the box, but you can select any UI library whichever you prefer. For the Nuxt modules, these are optional and you can select anything. For now I will select neither of these modules. Just hit ENTER key to continue without selection.

image.png

Same applies to Linting tools and Testing framework just nothing.

image.png

We will select rendering mode to be a Single Page Application, and deployment target would be Static since we do not need anything server side rendered. And for the development tools, if you select TypeScript you don't have to choose jsconfig.json otherwise you can. For the other options I will not select any of them for now.

image.png

Now to sum it all up regarding our installation selection, this is what we have.

image.png

Proceed to opening the new generated Nuxt project in your IDE (Visual Studio Code), do it via

$ cd nuxt-aws-amplify-todo-app && code .

This will then open up Visual Studio Code.

(Optional) Installing Nuxt Dependencies

These are optional but you can follow as well. I will be installing the following:

  • @nuxtjs/composition-api: This module is not recommended for production but soon it will be once Nuxt 3 comes out. When you are using this package, there will be bugs so keep that in mind in case you might get stuck then you can create an issue from the repository on Github and that counts as your contribution to the package.

Note: I will not cover how to setup this module but you can refer on the official docs instead.

  • @nuxtjs/google-fonts: I want to use "Sora" font just because FeedHive was using this and I think it's a cool font.

Note: I will not cover how to setup this module but you can refer on the official docs instead.

We'll also want to customize some of the default SCSS variables for Vuetify.js on some components and to apply "Sora" font-family as the main font to be used globally.

To customize, head on over to nuxt.config.js and on to the vuetify property, just add treeShake property with value of true, make sure to have treeShake set to true otherwise overriding SCSS variables for Vuetify will not work.

Also to verify you can reference on the code below on how I setup my nuxt.config.js file for these changes.

export default {
   // ... other properties
   buildModules: [
      // ... 

      '@nuxtjs/composition-api',
      '@nuxtjs/google-fonts'
   ],

   vuetify: {
      customVariables: ["~/assets/variables.scss"],
      treeShake: true
   },

   googleFonts: {
      families: {
         Sora: true
      }
   }
}

Then edit the variables.scss file and setup the following SCSS variables.

$body-font-family: "Sora", sans-serif;
$btn-letter-spacing: 0px !default;
$btn-text-transform: none;

And that's all we have for customizing the look for our Todo application we can then proceed with integrating AWS Amplify into our project.

Integrating AWS Amplify in the Nuxt project

Before we start, install the library so we'll have access to its core.

$ npm i aws-amplify

Now let us setup our backend using AWS Amplify. Given that you already have opened your Nuxt project right after installation then proceed to opening a new terminal instance under your IDE (Visual Studio Code) or if you prefer it in a different window that's fine as well but be sure you are in the root directory of your project.

Then execute this command to initialize or create a new AWS Amplify project.

$ amplify init

When presented with the questions, I have selected the default settings and modified only the "Source Directory Path" to . and for the "Build Command" is set to npm run build && npm run generate since we are going for a Nuxt Static SPA, and lastly the "Start Command" which is npm run start. I believe these build and start commands will be used if you prefer to host it on AWS ecosystem (S3 Bucket)

image.png

Once the initialization process is done, it should look similar to this by now.

image.png

Next and very important step, you must setup the aws-exports.js plugin. Create a file under plugins directory and name it as aws-amplify.js then copy the following code and paste on to it

import Amplify from "aws-amplify";
import aws_exports from "~/aws-exports";

Amplify.configure(aws_exports);

Then go to nuxt.config.js and add the following into the plugins array

export default {
   // ...

   plugins: ["~/plugins/aws-amplify"]
}

Since we are building a Todo app, then we will need a place to store our data. AWS Amplify can provide us a data store using DynamoDB. So let us continue setting up and add a backend API with the following command.

$ amplify add api

You can follow with my selection:

image.png

AWS Amplify already have an existing example for a Todo App so we'll use that one.

And once everything is set up correctly and is done, there are files generated for you with the GraphQL schema, resolvers, all those good stuff. So your project folder would look something like this by now.

image.png

Don't worry about the other files the were generated, you don't have to bother with them those are just the configurations files so that AWS Amplify will understand which server instance we are talking to behind the scenes. At least that's how I understand it.

We can take a look at our GraphQL schema that was auto generated by AWS Amplify for us.

image.png

Then let us proceed to deploying these into AWS. Let us execute the following command below so we can start making requests to the server via GraphQL.

$ amplify push

You will be then asked some questions to select from by the CLI.

image.png

To break it down for you, we tell CLI we want to auto generate GraphQL code, we select TypeScript as the output of the generated code, the filename pattern will reside in a graphql directory, we want AWS Amplify CLI to auto generate us all the possible operations/queries/resolvers that we might use for our Todo App, and the file name of the generated code will reside under api directory with file name as index.ts

Right after the installation process you will then be presented the GraphQL endpoint and the GraphQL API KEY which I don't want to expose it in here.

Now that it's all set up we can then proceed to building the UI of our app.

User Interface

I am just going to make it simple but of course feel free to build the UI in any form you want. For the UI we will have a form and a list that will list out the todos. Before we start building out the UI, open a server for the Nuxt app. In your terminal execute the command.

npm run serve

That will then provide you a URL for the app, typically it is http://localhost:3000 so open it up in your browser.

Once that is done, I made some modifications the following are:

  • I have deleted two files under components directory those are Logo.vue and VuetifyLogo.vue
  • I have deleted all the code for the app layout which can be found under layouts directory with file name of default.vue.
<template>
    <v-app app>
       <Nuxt />
    </v-app>
</template>
  • I have also deleted inspire.vue under pages directory.
  • And removed all code for index.vue under pages directory and replaced it with the following:
<template>
  <div></div>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";

export default defineComponent({
  setup() {}
});
</script>

It will be a grid layout, on the left side will be the form and on the right side will be the list.

<template>
  <div>
    <v-row>
      <v-col cols="12" xs="12" sm="12" md="4" lg="4" xl="4">
        <v-form></v-form>
      </v-col>
      <v-col cols="12" xs="12" sm="12" md="8" lg="8" xl="8">
        <v-list>
          <v-list-item></v-list-item>
        </v-list>
      </v-col>
    </v-row>
  </div>
</template>

Creating a Todo

Then we proceed to writing out the code to create our first Todo! Take a look at the code under.

HTML template,

<template>
  <div>
    <v-row>
      <v-col cols="12" xs="12" sm="12" md="6" lg="6" xl="6">
        <v-form ref="formRef" @submit.prevent="submit">
          <v-text-field
            label="Name"
            v-model="formData.name"
            :rules="requiredRule"
          ></v-text-field>
          <v-textarea
            label="Description"
            v-model="formData.description"
          ></v-textarea>
          <v-btn color="primary" class="my-5" large depressed type="submit">
            Submit
          </v-btn>
        </v-form>
      </v-col>
      <v-col cols="12" xs="12" sm="12" md="6" lg="6" xl="6">
        <v-list>
          <v-list-item v-for="(item, index) in todos" :key="index">
            <v-list-item-content>
              <v-list-item-title>{{ index }}</v-list-item-title>
              <v-list-item-subtitle>Description</v-list-item-subtitle>
            </v-list-item-content>
            <v-list-item-action>
              <v-btn color="transparent" depressed fab>
                <v-icon>mdi-delete-outline</v-icon>
              </v-btn>
            </v-list-item-action>
          </v-list-item>
        </v-list>
      </v-col>
    </v-row>
  </div>
</template>

TypeScript code,

import { defineComponent, computed, ref } from "@vue/composition-api";
import { createTodo } from "~/graphql/mutations";
import { API } from "aws-amplify";

type Todo = {
  name: string;
  description?: string;
};

const createTodoDtoDefaults: Todo = Object.freeze({
  name: "",
  description: ""
});

export default defineComponent({
  setup() {
    const formData = ref<Todo>({
      ...createTodoDtoDefaults
    });
    const todos = ref<Todo[]>([]);

    const formRef = ref();

    const requiredRule = computed(() => [
      (v: string) => !!v || "This field is required"
    ]);

    async function submit() {
      if (formRef.value.validate()) {
        await API.graphql({
          query: createTodo,
          variables: {
            input: formData.value
          }
        });

        // reset the `formData` values
        formData.value = {
          ...createTodoDtoDefaults
        };
      }
    }

    return {
      todos,
      formData,
      formRef,
      submit,
      requiredRule
    };
  }
});

By now we already have our form setup and it is using validation from one of the built-in feature of Vuetify's VForm component which is pretty convenient. A user can write a Todo now by typing in to the form, but whenever a user clicks the Submit button when the title field is empty, then it should not continue and therefore the app will not dispatch a call to the GraphQL API to create a new todo. Otherwise the Todo will be created!

Currently this is how the app looks from my end,

image.png

Fetching all Todo

When you tried to create a Todo by now, and sure there wasn't any data returned yet. So let us proceed to fetching all Todos from the API.

export default defineComponent({
  setup() {
    // ...

    const todos = ref<Todo[]>([]);

    // ...

    async function fetchAllTodo() {
      const response = await API.graphql({
        query: listTodos
      });
      // @ts-ignore
      todos.value = response.data.listTodos.items as Todo[];
    }

    async function submit() {
      if (formRef.value.validate()) {
        await API.graphql({
          query: createTodo,
          variables: {
            input: formData.value
          }
        });

        // reset the `formData` values
        formData.value = {
          ...createTodoDtoDefaults
        };
        fetchAllTodo();
      }
    }

    onMounted(() => {
      fetchAllTodo();
    });

    // ...
  }
});

So we defined fetchAllTodo() method that handles to fetch all Todos from the API by making a request to GraphQL using the query that was auto-generated for us! We will call this method in two places, under submit() method is when user's form input is valid and a Todo will be created then we should re-fetch the data to show the created Todo on the list. And the other place is from the onMounted life cycle, we are using composition functions so we call it from onMounted(() => {}) as that will handle calling to the API for fetching the Todos that are stored every time a User opens the app the first time.

So for the last time, this is the entire code for this Todo App.

<template>
  <div>
    <v-row>
      <v-col cols="12" xs="12" sm="12" md="6" lg="6" xl="6">
        <v-form ref="formRef" @submit.prevent="submit">
          <v-text-field
            label="Name"
            v-model="formData.name"
            :rules="requiredRule"
          ></v-text-field>
          <v-textarea
            label="Description"
            v-model="formData.description"
          ></v-textarea>
          <v-btn color="primary" class="my-5" large depressed type="submit">
            Submit
          </v-btn>
        </v-form>
      </v-col>
      <v-col cols="12" xs="12" sm="12" md="6" lg="6" xl="6">
        <v-list>
          <v-list-item v-for="(item, index) in todos" :key="index">
            <v-list-item-content>
              <v-list-item-title>{{ item.name }}</v-list-item-title>
              <v-list-item-subtitle>
                {{ item.description }}
              </v-list-item-subtitle>
            </v-list-item-content>
          </v-list-item>
        </v-list>
      </v-col>
    </v-row>
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
  computed,
  ref,
  onMounted
} from "@vue/composition-api";
import { createTodo } from "~/graphql/mutations";
import { API } from "aws-amplify";
import { listTodos } from "~/graphql/queries";

type Todo = {
  name: string;
  description?: string;
};

const createTodoDtoDefaults: Todo = Object.freeze({
  name: "",
  description: ""
});

export default defineComponent({
  setup() {
    const formData = ref<Todo>({
      ...createTodoDtoDefaults
    });
    const todos = ref<Todo[]>([]);

    const formRef = ref();

    const requiredRule = computed(() => [
      (v: string) => !!v || "This field is required"
    ]);

    async function fetchAllTodo() {
      const response = await API.graphql({
        query: listTodos
      });
      // @ts-ignore
      todos.value = response.data.listTodos.items as Todo[];
    }

    async function submit() {
      if (formRef.value.validate()) {
        await API.graphql({
          query: createTodo,
          variables: {
            input: formData.value
          }
        });

        // reset the `formData` values
        formData.value = {
          ...createTodoDtoDefaults
        };
        fetchAllTodo();
      }
    }

    onMounted(() => {
      fetchAllTodo();
    });

    return {
      todos,
      formData,
      formRef,
      submit,
      requiredRule
    };
  }
});
</script>

Conclusion

So we learned how to setup AWS Amplify for our Nuxt app with Composition API and TypeScript. If you have been planning to use AWS Amplify with Nuxt, then I hope this tutorial was helpful for you. However I might have only covered fetching and creating a Todo but as you see it the article has been very long, so I chose not to include those functionalities but you can explore more about AWS Amplify from the official documentation.

Thank you for taking the time to read and if you liked this be sure to like the post as well and if ever you want any future Nuxt tutorials like this let me know!

Full source code can be found from the repository for your reference