Building a Serverless Webapp with Amplify and Quasar - Day 2



We have been helping a customer to build an internal application using AWS Amplify for all-in-one hosting as well as CI/CD and Quasar as an UI framework. This work has led to a Meet-Up (virtual of course) where I helped as AWS expert.

If you want to follow along, these are my notes from the second session.

I assume that you have setup your environment like I showed in the first article. If not, clone my repository at branch day1 (git clone --branch day1 https://github.com/toelke/MyFirstQuasarAmplifyApp.git) and read on how to attach your own AWS account.

Should you already know all about quasar and web applications and are only here for AWS Amplify, click on “Finally. programming” above.


How to enable a new team-member

or: How to start by cloning a full repository

To start with a fresh check-out of a amplify repository, run amplify init:

Note: It is recommended to run this command from the root of your app directory
? Do you want to use an existing environment? No
? Enter a name for the environment dev
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS profile

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html

? Please choose the profile you want to use new-account
Adding backend environment dev to AWS Amplify Console app: dufr2td9r9ied
 Initializing project in the cloud...

The most important answer here is the No to the first question: If you answer “Yes” here, it will try to use an environment from the Amplify App that was rolled out by the initial owner of the code. Since you will most likely have no access to that account, you will get the error Could not initialize ‘dev’: Access Denied.

Of course, if we are talking about joining as a member of a team then you will want to answer “Yes”. You will be asked which environment to use. I recommend you use the environment “main”. When everything is set up, you can create your own environment with amplify env add. This way you will have your backend to test without disturbing your team-mates.

Some theory about HTTP and dynamic Web Applicatiions

HTTP is a rather old protocol to exchange “hypertext”, that is text containing links to other text. When reading about the original ideas for hypertext I feel that the best implementation right now is Wikipedia: A host of text with cross links to other relevant pages. But I digress.

HTTP is now understood to be the basic protocol of “the internet”1. It supports a number of operations (which are called “methods”) on content addressed by a path. A path is expressed as a hierarchy of strings, delimited by / just as in a file-system. For example, to get this article, your browser had to GET the path /2021/03/building-a-serverless-webapp-with-amplify-and-quasar-day-2.html from our webserver (which AWS is nice enough to rent to us).

The most important HTTP operations are subsumed under the name CRUD for “Create”, “Read”, “Update” and “Delete”. With these four operations you can support almost any task.

The methods for C, R, U and D in HTTP are PUT, GET, PUT, DELETE respectively. POST is also sometimes used for C and U.

In the olden days™ any dynamic web application would have all its code on the server2. Any user-input would be send to the server using a form-submission (either using the GET or the POST HTTP verb) which always resulted in a page-reload. This required an actual application server to run the application code. Since browsers now come with incredibly amount of javascript power, we switched to having much of the logic on the client. The javascript code “talks” to HTTP-APIs such as databases more or less directly. For hosting, you now only need your database, an API layer and some storage to host your static HTML, javascript and CSS from.

We are building a single page application (SPA) which means that all code and logic is encapsulated in a single HTML-file that the browser loads. All reactions to user actions are done in the browser and by exchanging data with APIs.

We will be using GraphQL as API-“Language”. GraphQL installs a layer above HTTP, so we will not be using exactly HTTP-methods for CRUD. All GraphQL-transactions POST a request to the API-Server, even for a Read operation. For storage (S3) we will be using pretty pure CRUD, though.

The directory structure of our project so far

amplify

The configuration and backend of amplify. See https://docs.amplify.aws/cli/reference/files

public

Everything in this directory will be put into the webserver as-is. This is the place to store images, fonts, …

See also src/assets below.

src/assets

These files will be packed into the application using webpack. Where you would access an image in public by referencing e.g. /static/img/foo.png, you would access an image in here by ~assets/foo.png.

src/boot

These files will be collected together into something resembling the main.js of Vue. The code here is run at the very start of the application. See here for more information.

src/components

This is were you put custom components. Depending on how we structure the Photo App, we might put the code here that shows an image.

src/css

This contains the CSS (or SCSS) of our web-page. Right now it only contains the quasar.variables.sass which you could now open and change the primary colors of your App.

src/i18n

This directory exists if you choose to add “i18n” (short for internationalization) to the App. Here we can put translations of our messages later.

src/layouts

This directory contains components that build up a page-layout. If you look at the MainLayout.vue-file, you will see that at one point it places the <router-view/>-component. This is there the actual page content will be placed. See this article for an explanation for the split between “page”, “layout” and “component”.

src/pages

This contains the content-pages of our App. Currently they are basically empty.

src/router

This configures the router of Vue. Here we will create “virtual” paths in our web-app, so that a user can share or bookmark a link to an album. The router will figure out from the link which album to show. You can read about the router here.

It boils down to “When it says /album/42 in the URL bar of the browser, what page should I render?”.

src/store

This contains files for the Vuex data store. You can use Vuex to transfer information about the current state of the App between components.

quasar.conf.js

This file configures Quasar.

Finally, programming

If it is hard to follow along my instructions on what to change how, have a look at the diff between day 1 and day 2.

As a reminder how Vue-files are structured: First there is a <template>-section which contains the HTML-code for the component. Next we have a <script>-section where we can place code. This section should export default an object with certain methods (like data, methods, …). Finally, we can have a <style>-section containing CSS. See also this introduction in the Quasar documentation.

Remove boilerplate from the frontend

Now to the frontend: Our App is very basic right now and supports no interaction. Also, the navigation is full of helpful links to the quasar documentation and other interesting stuff.

First, let’s remove the big quasar-logo in the middle of the page by editing pages/Index.vue:

<template>
  <q-page class="flex flex-center">
    <img
      alt="Quasar logo"
      src="~assets/quasar-logo-full.svg"
    >
  </q-page>
</template>
<template>
  <q-page class="flex flex-center"></q-page>
</template>

I also deleted the referenced file in assets/. As I wanted to keep the directory I then added a .keep file to it. This is necessary as git can’t track empty directories.

Now delete the component EssentialLink.vue from the components-directory (again, replacing it with a .keep-file). Also remove the complete <q-list> tag with all it’s content from layouts/MainLayout.vue:

<q-drawer
  v-model="leftDrawerOpen"
  show-if-above
  bordered
  content-class="bg-grey-1"
>
  <q-list>
	<q-item-label
	  header
	  class="text-grey-8"
	>
	  Essential Links
	</q-item-label>
	<EssentialLink
	  v-for="link in essentialLinks"
	  :key="link.title"
	  v-bind="link"
	/>
  </q-list>
</q-drawer>

Now delete the import EssentialLink from 'components/EssentialLink.vue', the const linksData = ...;, the item essentialLinks from the data model and the EssentialLink from the component object. Now the <script>-part of the Vue-file should look like this:

<script>
export default {
  name: 'MainLayout',
  components: { },
  data () {
    return {
      leftDrawerOpen: false,
    }
  }
}
</script>

When you take a look at the page now, it should basically look the same. The drawer should be empty and the big Quasar logo should be gone.

Add authentication to the backend

Now we tell amplify that we want to have user authentication:

amplify add auth

Which will ask us some questions:

Using service: Cognito, provided by: awscloudformation

 The current configured provider is Amazon Cognito.

 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections.
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.
Successfully added auth resource myfirstquasaramplifyb2956c4b locally<>

When we now do an amplify push, Amplify will create a Cognito Userpool for us. In my case the userpool is called myfirstquasaramplifyb2956c4b_userpool_b2956c4b-dev. Note the -dev at the end. This is the name of my current backend. We will see that with all the resources we are creating. When I create a user in my userpool later, that user will only be valid for my environment -dev. This ensures that I do not come into conflict with things in production (main environment) or with infrastructure of my team-mates.

If you like, you can now commit all changes (git add . && git commit -m 'added auth') and push them (git push). In a few minutes time you should see that the userpool for -main is created when Amplify runs the build as configured in the amplify.yml.

Add authentication to the frontend

We start by installing two new dependencies:

npm install aws-amplify @aws-amplify/ui-vue

aws-amplify is the library that we will use to communicate with Amplify resources such as Authentication or the API without having to program against the underlying AWS services. @aws-amplify/ui-vue is a collection of Vue components we can use in our App. We will be using the authentication components. See here for documentation.

When our App starts, we need to initialize the Amplify-library. To get cleaner code, we will encapsulate much of the calls to Amplify into their own file in a new directory called services. Create a file src/services/cloud.js:

import awsconfig from '../aws-exports';
import Amplify from '@aws-amplify/core';
import { Auth } from '@aws-amplify/auth';

export const configCloud = () => {
  Amplify.configure(awsconfig);
  Auth.configure(awsconfig);
};

We import the configuration file Amplify created for us and two libraries. The function configCloud will be called to initialize the Library when our App starts. So open the file App.vue and change it to be:

<template>
  <div id="q-app">
    <router-view />
  </div>
</template>
<script>
import { configCloud } from "src/services/cloud";
export default {
  name: "App",
  mounted() {
    configCloud();
  },
};
</script>

Now we can begin to use the Amplify library. Add this import to the <script>-section of MainLayout.vue:

import { onAuthUIStateChange } from "@aws-amplify/ui-components";

Also add two variables to the data-function: loggedIn: false and user: "". These will store whether a user is logged in or not.

Add a new block next to data():

created() {
  this.unsubscribeAuth = onAuthUIStateChange((authState, authData) => {
    if (authState == "signedin") {
      this.loggedIn = true;
      this.user = authData.username;
    } else {
      this.loggedIn = false;
      this.user = "";
    }
  });
},
beforeDestroy() {
    this.unsubscribeAuth();
},

The function created() will be called when our page is started. It subscribes to updates of the Auth module of Amplify. It will receive authStates like “signedin” when the user has successfully signed in or “signin” when the user should be asked for his sign in information. We use this information here to set the two state-variables loggedIn and user.

Likewise the function beforeDestroy() is called when the component is deactivated and we use it to clear our subscription.

For our next trick, we will simply import AWS' authentication-UI and install that in our page. Put the import

import '@aws-amplify/ui-vue';

at the top of the <script>-section, add

  formFields: [{ type: "email" }, { type: "password" }],

into the data of the component (I put it just after the user: "",-line) and

<amplify-authenticator v-if="!loggedIn" username-alias="email">
  <amplify-sign-up slot="sign-up" username-alias="email" :form-fields.prop="formFields" />
</amplify-authenticator>

just above the <q-page-container>-component in the <template>-section. As you can see, we only show this component if loggedIn is false, so if no user is logged in. We configure the sign-up dialog to only ask for email and password, otherwise it will also ask for a phone number.

Add v-if="loggedIn" to the <q-page-container> to only show this if the user is logged in.

When you now open the page (run amplify serve for a development-server), you should be asked for you password. You can now register a user. AWS will send you a confirmation mail. If all of this is successful, you no longer see the login screen. Next, we will do something so that it’s more evident that the login was successful. A logout-button for example! Add this block to the end of the <q-toolbar>:

<div class="absolute-right" v-if="loggedIn">
  {{ this.user }}
  <q-btn @click="logout()" flat label="LogOut" class="right" />
</div>

This will show the current user and a logout button if a user is logged in and nothing otherwise. When the logout-button is clicked the function logout() is called; we should create that next.

Add a new block methods to the export:

methods: {
  async logout() {
    const stat = await auth_logout();
    if (stat.status == 'ok') {
      this.loggedIn = false;
    }
  }
}

This calls a function auth_logout that we will add to our cloud.js and import:

// in MainLayout.vue
import { auth_logout } from 'src/services/cloud';
// in cloud.js
export const auth_logout = async () => {
  try {
    await Auth.signOut();
    const result = await Auth.currentUserInfo();
    return { status: 'ok', payload: {} };
  } catch (error) {
    return { status: 'error', payload: {} };
  }
};

Now it is possible to register in our App, to log in and to log out again. This is enough for today :-D Next week we will add actual UI elements, perhaps even put our first data into storage. If you want to join us, take a look at our MeetUp page!

See you in the next article.


  1. See https://twitter.com/brouhaha/status/1232139606981636096 ↩︎

  2. I am deliberately ignoring Java Applets and Flash. ↩︎

Similar Posts You Might Enjoy

Building a Serverless Webapp with Amplify and Quasar - Day 3

I was helping a customer with a Amplify App. This later turned into a meetup, where we are building a photo sorting application using AWS Amplify for the backend and Quasar for the frontend. This is the third article in a series that lets you follow along with the development process in your own time. - by Philipp Tölke

Building a Serverless Webapp with Amplify and Quasar - Day 1

I was helping a customer with a Amplify App. This later turned into a meetup, where we are building a photo sorting application using AWS Amplify for the backend and Quasar for the frontend. This is the first article in a series that lets you follow along with the development process in your own time. - by Philipp Tölke

Implementing optimistic locking in DynamoDB with Python

Concurrent access to the same items in DynamoDB can lead to consistency problems. In this post I explain why that is and introduce optimistic locking as a technique to combat this issue. - by Maurice Borgmeier