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.
Understanding what was generated
Preparing our setup for development
Implementing the interface
Connecting our components to our backend
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
A .nuxt directory that contains to compiled output
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
Open localhost:3000/todo without logging in first → you should be redirected to the login page
Type in some random login credentials and hit enter → the login should fail and you should still be on the login page
login with user and password → as we defined in our WebSecurityConfiguration
Add a new todo
complete the todo
delete the todo
add three more todos
Reload the page, your todos should still be there
The complete code after completing all parts can be found here.