This tutorial is deprecated!

This tutorial was published in 2020 and is no longer maintained. The code might not work as expected and to be honest I would have taken it offline if it wasn't for the fact that it still gets a lot of traffic. I hope you can still find some value in it. I have removed some parts that definetly don't work anymore. I personally really enjoy Nuxt 3 with tailwindcss and Vercel these days. Feel free to checkout my personal website.

The Frontend Using VueJS And NUXTJS

In this part we will initialise and program our web frontend. As with the backend, this part is divided into four subparts:

  • Choosing the framework

  • Initialising a NUXTJS project

  • Programming the frontend with additional changes to our backend

  • Building the frontend

Choosing the framework

Similar to the backend there are hundreds of ways to accomplish what we set out to do. I worked with angularjs and a little with angular before but in general I don't have any experience with modern js-frontend-frameworks at this point. If we look at the three main frameworks angular, vuejs and react one might argue which is better but I think it is commonly agreed on that they are all good, modern frameworks. For this series I went with vuejs just because I liked the fact that is completely community driven.

However, before we start lets take a step back. In general frontend frameworks, especially Java-Script-Frameworks gained significant popularity over the last couple of years. However, most of them rely heavily on the client, as the side is build dynamically using javascript. This has two main disadvantages:

  • The side is not SEO friendly

  • The performance of the side depends on the performance of the client

The solution to this is to create most of the html, css and js on the server. These applications are known as server-side-rendered (SSR) applications. Most of the modern js-frameworks offer ways for SSR.

For vuejs there is a framework called NUXTJS for SSR.

Initalising a NUXTJS project

NUXTJS uses nodejs to run javascript on the server. It also uses nodes package manger npm for dependecy manger. Please note, that you can also use yarn but we will go with npm here. To initialise a NUXTJS project, make sure you have installed a recent nodejs version and run the following command on the terminal (in your projects root folder):

This initialiser will then ask us a couple of questions. As before the point of this series is to keep everything as simple as possible. We will select:

  • frontend as the project name

  • whatever description you like

  • your name as author

  • NPM as package manager

  • No UI Framework

  • No custom server framework

  • Axios as module for network requests (use the spacebar to select)

  • ESLint to help us with formatting or code (we will look into that in a bit - again select using the spacebar)

  • No test framework

  • SSR for rendering

  • Select jsconfig.json (using the spacebar) if you are using vs code (like I do)

Programming the frontend

Now that we have initialised or project, lets program the frontend. We will do that in four steps.

  1. Understanding what was generated

  1. Preparing our setup for development

  1. Implementing the interface

  1. Connecting our components to our backend

  1. The login page

Understanding what was generated

Let's open the project in our favourite code editor. For me that Visual Studio Code. Lets have a look what was generated. As with the backend we will look at every folder and file in the root directory. There is actually a really good documentation about the folder and what they should be used for . That's why I will just go very briefly into it here.

  • Assets → Any kind of files that should be compiled and are needed in our project (e.g. stylesheets, fonts)

  • Components → That's where we store our vuejs components. A component is a reusable vue instance (e.g. footer, navigation, todo-item).

  • Layouts → Layouts are used to customise the look and feel of our pages. We will only use the default layout in this tutorial.

  • Middleware → Here we can define function that run before pages are rendered. We will not use middlewares in this tutorial.

  • node_modules → All of our dependencies (see package.json) are stored here

  • Pages → The pages of our application.

  • Plugins → Here we can and will define plugins that are run before initialising our vuejs application.

  • Static → Place for static files, which we will not have.

  • Store → If we were to use vuex store, the files would go here

  • .editorconfig → This files just provides some settings for our code editor, such as that trailing whitespaces should be deleted

  • .eslintrc.js → This is where we configure ESLint. So what is ESLint?

    • ESLint basically looks at your code and checks if it conforms to predefined rules in terms of formatting and code style

    • Our eslint config file contains five blocks

      • Root → This tells eslint that the config file is located in the root directory of the project. Parent files and directories are not considered. Further documentation can be found here.

      • Env → Here we define the environment of the project. In our case our project is for the browser and uses node

      • ParserOptions → Here we set the parser for eslint. As NUXTJS is using babel under the hood to build our application, we use the babel-parser here.

      • Extends → Here we define sets of rules that our project uses

      • Rules → Here we could define additional rules

  • .gitignore → Files and folders to be ignored by git

  • jsconfig.json → Settings for VS Code

  • nuxt.config.js → Here we configure nuxt. As this is documented pretty good with inline-comments, I won't go into it.

  • package.json → Here we configure our nodejs-application. We set basic attributes such as name, version. Additionally we define scripts, dependencies and devDependencies.

    • Scripts can be executed via npm run <script-name> and execute the command respectively

    • Dependencies are the modules that we need to run our app. They are then pulled from the npm-registry and stored in node_modules. The version can be defined using special syntax, documented here.

    • devDependencies behave just like dependencies, only that there are not needed to run the app, only to develop it. More on that here.

  • package-lock.json → This file contains every single dependency and the exact version number used. That way you can rebuild a project and reproduce errors someone else might have gotten more reliable.

  • README.md → The readme file.

After we run our app at least once there will also be

Preparing our setup for development

Before we start to program our application, we will do further configuration to ensure a smooth development workflow.

ESLint auto-fix on save

When I first started programming the app I found the ESLint errors immensely annoying, as you can't use your application unless all the errors are fixed. However, there is a way to automatically fix all ESLint errors on save. All we have to do is go to our nuxt.config.js file and replace the current extend method by the following.

Styling using Sass

In this tutorial I will not explain the css we use. To ensure your app looks and feels the same, I will provide you with the complete stylesheet every time we create a new layout, page or component. We could just use plain css but I prefer Sass, as it is more powerful, thanks to variables, nesting and so on. As Sass has to be compiled (converted into plain css) we have to add a dependency for development. Do so by running the following command in your terminal inside the frontend project folder:

Running our application

We can run our application in development mode by executing the following command on our terminal inside the frontend folder. The frontend is then accessible from the browser at http://localhost:3000

Cleaning up

Nuxt provided us with a base setup, which is nice but we will just get mostly rid of it.

  • Delete default.vue in layouts

  • delete index.vue in pages

  • delete logo.vue in components

Congrats, our application is now broken :)

Implementing the interface

To work with our backend we want to have a login page, as well as an interface to list, create, delete and complete todos. This tutorial does not aim to build a perfect todo app. The purpose is to have a base set up that can easily be used for other projects. That's why our focus is to work with data from our api.

We will have

  • one layout for the entire app

  • one page for the login → we will do this at the very end, when connecting our backend

  • one page to work with todos

  • three componets

    • one to create todos

    • one that acts as a container for existing todos and provides us with an interface to create todos

    • one for every single todo

The layout - default.vue

Nuxt uses vues single file components. In every .vue file we have a template section (our html) an optional script section (our javascript) an optional style section (our css).

Create a new layout called default.vue in the layout folder and insert the following code:

As I said I will not talk about styling. Our template section is really straight forward as we just define a div element where our NUXTJS app is rendered into.

The todo page - todo.vue

Create a new file called todo.vue in the pages folder and insert the following code:

Let's go through the template and script section.

<template>

  • There is nothing really happening here, besides a wrapper and a headline.

<script>

  • The data function returns an object that we can use in our template. We will need to work with an array to store our todos. We can not make any http request here.

  • The asyncData function is used to fetch data, which will then replace the corresponding variable in the data block. For now we will use dummy data but instead of a static array, this is where we are going to call our api. AsyncData is called whenever a page is loaded.

If we open our application in the browser we will just see our headline.

However, if we download and open the vue extension for chrome we can see that the asyncData method injected the objects in our todo array.

Component I - ToDoList.vue

This component is responsible for managing our todos.

  • It will be provided with the initial todos from the backend

  • It will display all todos using another component

  • It will handle the creation of new todos

Create a new file called ToDoList.vue in components and insert the following code.

Lets see what is happening here.

<template>

Besides a few containers there are only two elements worth mentioning.

  • The input

    • The input element is used to give new todos a title

    • To store and access the typed title we link the input to a property of our data object called titleOfNewToDo

  • The button

    • The button is used to actually create the todo

    • We want vue to trigger a method (called create) when this button is clicked

  • The input

    • The input element is used to give new todos a title

    • To store and access the typed title we link the input to a property of our data object called titleOfNewToDo

  • The button

    • The button is used to actually create the todo

    • We want vue to trigger a method (called create) when this button is clicked

<script>

Components work a little different than pages. We can not fetch any data here using asyncData. If we want to provide our components with inital data, we have to pass it using properties (props). In our case we need a property for our todo of type array. If our component is not provided with an array we default to an empty array. The title of a new todo is stored in the returned object of our data function.

So far we have created our new component but we are not using it. Lets go back to our todo.vue page and add the component. To do so we need to register the component in our script section.

We can then use the component in our html and pass the todos array as property to the component.

If we refresh our app (with /todo) in the browser we can see the input field to create new todos. It isn't working yet but it is there.

Also there aren't any todos. So let's create a new component.

Component II - ToDoListItem.vue

This component is responsible for handling a single todo.

  • It will display a single todo

  • It will handle the completion of this todo

  • It will handle the deletion of this todo

Create a new file called ToDoListItem.vue in components and insert the following code.

Lets go through this.

<template>

There are three things worth point out here.

  • Our wrapper div will be given the class completed if the property completed of our todo is set

  • We can display properties of our todo by using curly brackets and our todo object (e.g. todo.title)

  • Analog to our create method we bind two functions to a span element in the event of a click

<script>

Analog to our todo-list-component we declare a property of type object to store our todo.

Lets go back to our ToDoList component to actually use our new component. For the script section we make the following changes.

In our template section we add the component link this

Our component has three attributes

  • We use the v-for declaration to create this component for each todo in our todos array

  • The :key attribute lets vue know which property of our todo uniquely defines it (this is not mandatory but considered best practise)

  • The :todo attribute sets the todo property in our ToDoListItem component to the corresponding todo

  • The v-on:delete attribute tells the component to call its deleteItem method if the child component (to-do-list-item) raises the event "delete" → We will talk more about this in a second

If we refresh our app we can actually see our todos. However, we can not create, complete or delete todos.

Creating new todos

To create new to dos we have to implement the create function in our ToDoList component. We do this in the script section.

As with data, we create a new object for methods and define the functions in there. For now our create function we add to lines. We will change that once we connected our backend. At this point it is just a proof of concept.

  • Our create method adds a new object to our todos array. Id and title are the current value of our input field. Completed is false.

  • We then reset the input field.

We are now able to create new todos, locally at least.

Completing and deleting todos

To complete and delete todos we need to make changes in our ToDoListItem component.

  • Completing a todo

    • To complete a todo we simply set the completed-property to true

    • Vue will then automatically assign the completed css class to the element since the property has changed

  • Deleting a todo

    • We could just delete the item by removing the element, however this is not good practice as we want our data object (the array of todos) to be consistent. We therefore want to remove the todo from the array. Vue will notice that the item no longer exsists and remove it.

    • Our ToDoListItem component does not have access to the array of all the todos. To remove the item, we have to communicate with the parent component. By calling the $emit method, we do exactly that. We trigger the event "delete" and pass the todo object to it.

    • In our ToDoList component we add a method block in the script section.

    • As you may recall we added a v-on attribute to the component stating that whenever the event "delete" is called, we want to trigger the "deleteItem" method.

    • The method simply removes the item from the array

When we reload the app in the browser we now find our interface fully working.

Refactoring the to do item creation

Now that we know how to use components and how to communicate between them, it seems wise to also move the creation of todos to a seperate component.

Create a new component by the name ToDoItemCreate.vue and insert the following code:

The template is the same as before in our ToDoList-component. In the script section we have the titleOfNewToDo as data attribute and similar to the deletion of todos we call the event create in our parent component.

Therefore we have the following changes in our ToDoList component.

In the template section we replace the div "create-container" with the component.

In the script sections we refactor accordingly:

Now our app is even cleaner and should still work as well as beofre.

Connecting our components to our backend

So far our frontend works but it is isolated and resets on every reload. We now want to connect our frontend to our backend. Before we switch back to the frontend make sure that your MonogDB and your backend is running as we will now try to access it.

Implementing the API

Now we can switch back to the frontend. We will use axios for making web request to our backend. We also need the proxy module. Install it as npm dependecy using

Let's start by configuring the url of our backend in axios. Open the nuxt.config.js file and replace the axios part as followed.

The reason why we use the proxy module here is that we want to be able to run front- and backend from different domains. Without the use of the proxy module this would not be possible, let me explain.

When we log in, we make a request from our browser directly to our backend. Our backend then tells the browser to use cookies for every request that goes to the backend. This is where the problem lies. Since we are rendering parts of our frontend from the server-side, we make requests to the backend that are not directly triggered by the browser. They are triggered by our frontend-server-side. These request will not carry any cookies because this is not the same host that was used when we logged in. In other words, the cookies can only be used for request directly from the browser.

To solve this problem we have to make every single requests from the server-side. Even request directly from the browser should go to our frontend-server and should then be redirected / proxied.

This is why our configuration results in the following behaviour:

  • All requests in our app have the prefix localhost/api/

  • All requests to ***/api/*** are proxied to http://localhost:8080/api/

Right now this does not make a difference as it is always localhost. However, this will change once we deploy the application.

Now we can work with the data. Create a class in javascript for our todos. Create a folder in assets called data and a new file called ToDo.class.js with the following content.

We could implement our api calls in every page and component where we need to. Yet, I prefer to bundle all our calls to the api in one file. That way it can be maintained and reused more easily. For that we create another folder in assets called service containing a new file called ToDoService.js with the following code.

Okay so there is a lot going on here, don't panic. Lets go through it step by step.

  • First we import our newly created ToDo class

  • We then define a constructor that takes in the $axios object

  • Then we define the functions (endpoints) of our API

    • findAll

      • Here we send a get request to todo

      • The option "withCredetilas" tells axios to send the tokens we acquired through the login with the request

      • We convert the array of objects to an array of todos using our class and return it

    • create

      • The creation of todos is pretty similar to getting the todos

      • Instead of get we will perform a post request

      • The method body contains the title and completed (which should always be false)

    • complete and delete

      • These request are similar to create

      • They differ in the request type (put and delete) and they use the id of the todo to dynamically extend the url

That wasn't that complicated, was it? Now you may ask yourself how we can access our api in our components. For that we need to expose it. We do that by creating a plugin called services.js in plugins. The new plugin contains the following code

  • We firstly import or ToDoService

  • In our main plugin-code we define a new object called services and add the ToDoService.

  • The idea is to define a service for every data type and then to simply add it here

  • We finally inject the services object under the name services so we can use it everywhere

We now need to register the plugin in the nuxt.config.js file in the plugin section.

What about errors?

It might happen that the request to our backend fails. This can happen for a number of reasons, the most simple one is that the internet connection of the user drops. We could add a catch block to every request in our Api class. This should be done if we need to handle a specific error individually. However, it also makes sense to bundle all errors, to handle the most basic ones in one central place. We do this by using another plugin, the interceptor.js.

  • We extract the status code and message of the error

  • If it is a 401 error we simply redirect to our login (index) page → that we still have to create

  • If it is any other error we just throw it. In a real application the errors should obviously be handled much better. However, as this project is about the setup, we are done.

Again, we need to register the plugin in the nuxt.config.js file.

The login page

We are now at the point where we would access our backends api. However, it would not let us in, due to our security configuration. We therefore need a login page. Create a new page in pages called index.vue and insert the following code:

Lets start with the template section:

  • We create a form with to inputs

  • Both inputs are binded to a vue property using the v-model directive

  • When submitting the form we will call the performLogin method and also prevent the browser from executing any default behaviour

In the script section we added a method for the login

  • We call a method from a login service (which we will create in a second)

  • If the method returns true, we redirect to the todo page

  • If the method returns false, we simply reset the input fields

Next, let's create a new service in assets/service called LoginService.js and insert the following code:

This should be vrey straightforward. We perform a post request and return either true or false based on the response. As you may recall, we configured our backend to simply return 200 in case of a successful and 401 in case of an unsuccessfull login.

Lastly, we got to inject our new service, by updating our our services.js plugin.

Go ahead, and test the login it should work fine.

Accessing the API

Okay after all the preperation it is now time to use our repository. Due to our setup this is incredibly simple.

In our todo.vue page we simply do the following to get our todos from the api.

Our app now displays the entries from our MongoDB database using our Spring backend. Awesome!

Now lets also implement todo creation, completion and deletion.

Creation

Navigate to ToDoListCreate.vue and replace our creation method with this.

Simple, eh?

Completion

Navigate to ToDoListItem.vue and replace our completeItem method.

Deletion

In the same component we also change the deleteItem method.

Testing the application

  1. Open localhost:3000/todo without logging in first → you should be redirected to the login page

  1. Type in some random login credentials and hit enter → the login should fail and you should still be on the login page

  1. login with user and password → as we defined in our WebSecurityConfiguration

  1. Add a new todo

  1. complete the todo

  1. delete the todo

  1. add three more todos

  1. Reload the page, your todos should still be there



The complete code after completing all parts can be found here.

Part I: The Backend with Java & Spring

Part III: Dockerizing Our Front- & Backend