• Home
  • Blog
  • Frontend for backend developers: an introduction to Nuxt.js – Part 2

TRENDS

03.08.2020 - Read in 9 min.

Frontend for backend developers: an introduction to Nuxt.js – Part 2

03.08.2020 - Read in 9 min.

Working on the frontend layer may seem like an abstract concept to backend developers. Previously, we managed to install a Nuxt.js framework and configure it to our needs. Now, it’s time to create the frontend layer using this tool.

RST Software Frontend for backend developers: an introduction to Nuxt.js - Part 2

Previously, we managed to install a Nuxt.js framework and configure it to our needs: Frontend for backend developers: an introduction to Nuxt.js. Now, it’s time to create the frontend layer using this tool.

 

Communication with an external API

You can use an external service based on the REST API architecture to download data that will be displayed on your website. As backend devs, we have mastered creating new CRUD compliant resources, right?

That’s why, for this frontend, we’ve prepared a backend compliant with REST API, available under example URL: https://api.example.com. The service deploys two endpoints built like this:


// GET https://api.example.com/api/v1/recipes 
{
  "items":
  [
    {
      "icon": "https://images2.imgbox.com/90/76/9F7EcUON_o.jpg",
      "title": "Mojito (mohito)",
      "id": "mojito",
      "description": "Słodki i orzeźwiający drink na bazie białego rumu wraz z limonką, brązowym cukrem i miętą.",
      "rate": 4.5,
      "rates": 413,
      "prepare_time": "10 min",
      "prepare_difficulty": "łatwy"
    },
    {
      "icon": "https://images2.imgbox.com/90/76/9F7EcUON_o.jpg",
      "title": "Old Fashioned",
      "id": "old-fashioned",
      "description": "Legenda wielu barów. Whiskey w najlepszym wydaniu.",
      "rate": 4.9,
      "rates": 759,
      "prepare_time": "10 min",
      "prepare_difficulty": "łatwy"
    }
  ],
  "page": 1,
  "total_pages": 300
}

// GET https://api.example.com/api/v1/recipes/old-fashioned
{
  "icon": "https://images2.imgbox.com/90/76/9F7EcUON_o.jpg",
  "title": "Old Fashioned",
  "id": "old-fashioned",
  "description": "Legenda wielu barów. Whiskey w najlepszym wydaniu.",
  "rate": 4.9,
  "rates": 759,
  "prepare_time": "10 min",
  "prepare_difficulty": "łatwy"
}

With the Node.js-based code, we’ll use the axios library for http(s) communication. It’s currently the most popular solution for such tasks, which means it’s also automatically supported by many frameworks.

Assuming that we have an API running at the https://api.example.com URL to deliver recipe-related data as https://api.example.com/api/v1/recipes resources for the collection and https://api.example.com/api/v1/recipes/[ID] as information about specific recipes, we can now configure our external service as a Nuxt.js proxy. With this, all requests sent by the frontend will be forwarded to your own domain and all communications with the service that delivers drink recipes will be server-side, solving potential CORS issues.



// nuxt.config.js
{
  // ...
  modules: [
    // ...
    '@nuxtjs/axios',
  ],
  axios: {
    proxy: true
  },
  proxy: {
    '/api/': { target: 'http://api.example.com/', pathRewrite: {'^/api/': ''} }
  }
}

Of course, you can use a similar method to support many independent services that deliver content to your frontend. All you have to do is register another proxy URL for another service. Piece of cake!

The instance of our library that handles http(s) requests is automatically available in all important elements of the Nuxt.js system (e.g. components).

 

A communication mechanism configured this way will be needed to handle data displayed in views. For that purpose, we also have to prepare a data access layer that will deliver data in a unified structure.


// src/repositories/RecipesRepository.ts

import axios, { AxiosInstance } from 'axios';

interface RecipeModel {
  id: string
  icon: string
  title: string
  description: string
  rate: number
  rates: number
}

interface RecipesRepositoryInterface {
  getCollection(page: number, perPage: number): Promise<RecipeModel[]>

  getById(id: string): Promise

  countCollection(): Promise
}

const recipesRepositoryClient = axios.create({ headers: { 'accept': 'application/json' } });

class RecipesRepository implements RecipesRepositoryInterface {
  public constructor(private client: AxiosInstance) {
  }

  public getCollection(page: number = 1, perPage: number = 12): Promise<RecipeModel[]> {
    return this.client
      .get<{ items: RecipeModel[] }>('/api/recipes')
      .then((res) => res.data.items)
      .catch((err) => {
        console.warn(err);
        return [];
      });
  }

  public getById(id: string): Promise {
    return this.client
       .get('/api/recipes/' + id)
      .then((res) => res.data)
      .catch((err) => {
        console.warn(err);
        return null;
      });
  }

  public countCollection(): Promise {
    return this.client
      .get<{ total_pages: number }>('/api/recipes')
      .then((res) => res.data.total_pages)
      .catch((err) => {
        console.warn(err);
        return 0;
      });
  }
}

export const repositoryRecipe = new RecipesRepository(recipesRepositoryClient);

The code above handles several functions:

  • It defines the interface of the repository that handles recipe data delivery.
  • It defines the data model that will be used in the frontend app (for convenience, it’s identical to the API data model).
  • It creates a repository implementation using the Axios libraries for communication.

As you can see, we’re writing frontend code, but so far it looks and behaves just like the backend! We are using a class definition, interface implementation… and we even have a design pattern here—the repository.

 

Views and templates: what’s visible to the user and why

There are two elements that influence how content is displayed in Nuxt.js.

  1. The first element are the controllers directly linked with routing. This means that the files responsible for the view of each page are directly reflected in file structure. In our case, they are located in src/pages and src/layouts. Here’s an example: the direct view of URL that displays a specific recipe http://example.com/recipes/12345 is located in src/pages/recipes/_id.vue or src/pages/_id/index.vue. The documentation offers a great explanation of this structure. However, nothing’s stopping you from breaking the rules and setting up custom routing.
  2. The second element—which we’ll use to render view elements—are Vue.js components: reusable, configurable elements responsible for displaying every component of a website. The Vuetify library we chose during project configuration in the previous part is simply a set of predefined components that match one another in such a way that view elements can be grouped together into consistent, complex graphic interfaces.

From the technical point of view, the first element (a controller) is also a component in Vue.js terms. Without going into details, it means that all elements of a view have a unified code structure. Views (controllers) are simply directly linked to routing and are our point of entry into the app.

Frontend for backend developers: an introduction to Nuxt.js — Part 2

Page view

The preparation of our page’s frontend begins with the homepage. After going to src/pages/index.vue we start creating a template of our homepage where a list of several recently added recipes will be displayed.

We need to write suitable code:


<template>
  <v-container>
    <v-row>
      <v-col cols="12" md="6" lg="4" xl="3">
        <v-card class="mx-auto" max-width="374">
          <v-img height="250" src="https://images2.imgbox.com/90/76/9F7EcUON_o.jpg"></v-img>
          <v-card-title>Mojito (mohito)</v-card-title>

          <v-card-text>
            <v-row align="center" class="mx-0">
              <v-rating :value="4.5" color="amber" dense half-increments readonly size="14"></v-rating>

<div class="grey--text ml-4">4.5 (413)</div>

            </v-row>

<div>Słodki i orzeźwiający drink na bazie białego rumu wraz z limonką, brązowym cukrem i miętą.
</div>

          </v-card-text>
        </v-card>

      </v-col>
    </v-row>
  </v-container>
</template>

The HTML code we want to render is in the <template> tag. It’s one of the three tags we use to describe the behaviour of our webpage. The other two are: <script>, which stores the JavaScript that configures the view behaviour, and <style>, which is responsible for CSS styles of the rendered HTML snippet.

All the tags in the code above can be treated like functions in backend development. They take input parameters, perform a specific action, and render an output response. If a tag is not implemented (isn’t a component), it is treated like regular HTML.

This code displays a block with a recipe that includes a photo, title, description, and rating. Right now, we only have one block, but it’s already adapted to various resolutions and mobile devices by using the Vuetify library’s grid system directly based on flex-box.

Frontend for backend developers: an introduction to Nuxt.js — Part 2

Not bad for just a few lines of HTML!

Let’s see how our view will look when we add more recipes to the mix. We can use the previously-made library to handle REST API, updating the rendered HTML in the src/pages/index.vue file:


<template>
 <v-container>
   <v-row>
     <v-col cols="12" md="6" lg="4" xl="3" v-for="(recipe, i) in recipes" :key="i">
       <v-hover v-slot:default="{ hover }">
         <v-card class="mx-auto" max-width="374" :to="'/recipes/'+recipe.id" nuxt :elevation="hover ? 16 : 2">
           <v-img height="250" :src="recipe.icon"></v-img>
           <v-card-title>{{recipe.title}}</v-card-title>

           <v-card-text>
             <v-row align="center" class="mx-0">
               <v-rating :value="recipe.rate" color="amber" dense half-increments readonly size="14"></v-rating>

<div class="grey--text ml-4">{{recipe.rate}} ({{recipe.rates}})</div>

             </v-row>

<div>{{recipe.description}}</div>

           </v-card-text>
         </v-card>
       </v-hover>
     </v-col>
   </v-row>
   <v-row>
     <v-col cols="12" class="text-center">
       <v-pagination
         v-model="page"
         :length="total"
         @input="updatePagination"
         total-visible="10"
       />
     </v-col>
   </v-row>
 </v-container>
</template>

<script>
 import { repositoryRecipe } from "../repositories/RecipesRepository";

 const PER_PAGE = 6;

 export default {
   methods: {
     async updatePagination(page) {
       this.recipes = await repositoryRecipe.getCollection(page, PER_PAGE);
     }
   },
   async asyncData() {
     return {
       recipes: await repositoryRecipe.getCollection(1, PER_PAGE),
       total: await repositoryRecipe.countCollection(),
     }
   },
   data() {
     return {
       page: 1,
       total: 100
     }
   }
 }
</script>

We have used the previously prepared repository that fetches data from REST API, and then we display the collection by rendering the HTML code.

Both data and asyncData methods are responsible for creating an object representing the data stored by the current component. The difference is that asyncData lets you fetch this information in an asynchronous way from external resources. Simultaneously, the data is fetched and rendered on the server side at the initial visit of the user. Have I mentioned the importance of SEO?

Note that from now on, the blocks are fully-fledged references that direct you to the page containing the details of a given recipe due to the
:to=”‘/recipes/’+recipe.id” nuxt code within the <v-card> component.

As a result, we have a pretty neat page with recipes:

Frontend for backend developers: an introduction to Nuxt.js — Part 2

Let’s now proceed to everything that surrounds our content, i.e. the header and the footer. The black colour here is not too cheerful either and does not match the character of our frontend.

The first thing to do is to restore the default colours for our graphics library. To do that, you need to modify the vuetify section of the nuxt.config.js file:


vuetify: {
  customVariables: ['~/assets/variables.scss'],
},

 

The views of individual routings are packed into the global template. The template is responsible for rendering common components for all pages, and in our case it’s located in the src/layouts/default.vue file (a component again, which is great news as we already know how to work with those!).

Let’s change its code to something more apt:



<template>
  <v-app>

<div class="app-header">

<div class="text-center pa-5">
        <nuxt-link to="/">
          <img src="/logo.png" alt="logo">
        </nuxt-link>
</div>

</div>

    <v-content>
      <v-container>
        <nuxt/>
      </v-container>
    </v-content>
    <v-footer fixed app>
      <span>&copy; {{ new Date().getFullYear() }}</span>
    </v-footer>
  </v-app>
</template>

<style scoped>
  .app-header {
    background-color: #1d3556;
  }
</style>

The <nuxt/> component shows the location of rendering all the views responsible for routing. Apart from cleaning the template code, we have also defined a new header with a logo. The graphic file is located in the src/static/logo.png directory and it can be embedded in the HTML code, as presented in the example above.

Apart from the graphic file, the template also includes a defined CSS style named app-header, which is defined in the <style> tag with scoped attribute. The scope definition affects the visibility of this CSS class selector. In this case, the app-header class will only work for the code within this component. This is one of the best practices of coding in Vue.js.

 

To meet all the requirements, now we just need a single recipe view.

Let’s create the template for Nuxt.js routing i.e. src/pages/recipes/_id.vue:


<template>
 <v-container>
   <v-row>
     <v-col cols="5">
       <v-card rounded elevation="12">
         <v-img :src="recipe.icon"></v-img>
       </v-card>
     </v-col>
     <v-col cols="7" class="blue-grey--text text--darken-4">

<div class="display-3 py-4 font-weight-bold">
         {{recipe.title}}
</div>


<div class="blue-grey--text text--darken-2">
         {{recipe.description}}
</div>


<div>
         <v-container>
           <v-row class="text-center">
             <v-col cols="5">
               <v-icon style="font-size: 72px;">mdi-clock-check-outline</v-icon>

<div class="pt-3">

<div class="font-weight-bold">Czas przygotowania</div>

                 {{recipe.prepare_time}}
</div>

             </v-col>
             <v-col cols="1">
               <v-divider
                 class="mx-4"
                 vertical
               ></v-divider>
             </v-col>
             <v-col cols="5">
               <v-icon style="font-size: 72px;">mdi-account-cog-outline</v-icon>

<div class="pt-3">

<div class="font-weight-bold">Trudność wykonania</div>

                 {{recipe.prepare_difficulty}}
</div>

             </v-col>
           </v-row>
         </v-container>
</div>

     </v-col>
   </v-row>
 </v-container>
</template>

<script>
 import { repositoryRecipe } from "../../repositories/RecipesRepository";

 export default {
   validate({params}) {
     return repositoryRecipe.getById(params.id);
   },
   async asyncData({params}) {
     return {
       recipe: await repositoryRecipe.getById(params.id),
     }
   }
 }
</script>

We use the previously prepared components from the Vuetify library. Each component has an excellent documentation comprising a description and a visualisation of all parameters. What happens in the JS part of the code is even more interesting. Firstly, we use the validate function, which is run both on the server and client side, and automatically validates if our recipe base includes a recipe represented by a given identifier. If such a recipe does not exist, a 404 error page is automatically displayed. The other method, i.e. asyncData, loads data about a given recipe and stores it in the variable used for rendering the view. Easy, isn’t it?

Now, you can check and see how your PoC of the frontend in Nuxt.js looks and behaves.

Frontend for backend developers: an introduction to Nuxt.js — Part 2

 

Vue.js components in a glimpse

When defining individual views, we were using the components — the true superpower of Vue.js. As I have mentioned before, each view of a page or a template is in practical terms a component.

Components are responsible for the preparation and displaying of portions of a template visible to the end-user. They can vary in terms of the HTML code and the JS logic. There is a style guide and a collection of best practices on appropriate preparation of components.

The components can communicate with one another in three different ways:

  • from parent to child — using the props component. We were using this mechanism the entire time when we were creating views, defining the parameters of rendered components. For example, using <v-col cols=”5″> means that we pass a parameter called cols with a value of 5 into a component called <v-col>. The component that receives the parameters defines the names and types of parameters we can pass.
  • from child to parent — using events. The child responds to a specific behaviour by emitting an event with a specific name and parameters. The parent calls the child component and defines its function that listens for the event and reacts to it by e.g. modifying its state.
  • everything with everything — this can be achieved using an additional library/manager to handle Vuex states. To put it simply, you can treat this solution like a global register where every component can call for information or modify the state of such a register by using dedicated mechanisms.

Let’s create our first, basic component src/components/RecipeGridItem.vue


<template>
  <v-hover v-slot:default="{ hover }">
    <v-card class="mx-auto" max-width="374" :to="'/recipes/'+recipe.id" nuxt :elevation="hover ? 16 : 2">
      <v-img height="250" :src="recipe.icon"></v-img>
      <v-card-title>{{recipe.title}}</v-card-title>

      <v-card-text>
        <v-row align="center" class="mx-0">
          <v-rating :value="recipe.rate" color="amber" dense half-increments readonly size="14"></v-rating>

<div class="grey--text ml-4">{{recipe.rate}} ({{recipe.rates}})</div>

        </v-row>

<div>{{recipe.description}}</div>

      </v-card-text>
    </v-card>
  </v-hover>
</template>

<script>
  export default {
    props: {
      recipe: {required: true}
    }
  }
</script>

As you can see, this is a snippet of the code responsible for rendering a single block in the grid of recipes. You pass one required parameter—called recipe— into the component.

Now, let’s proceed to our page with the grid (src/pages/index.vue) and use the new component:


<template>
 <v-container>
   <v-row>
     <v-col cols="12" md="6" lg="4" xl="3" v-for="(recipe, i) in recipes" :key="i">
       <RecipeGridItem :recipe="recipe"></RecipeGridItem>
     </v-col>
   </v-row>
   <v-row>
     <v-col cols="12" class="text-center">
       <v-pagination
         v-model="page"
         :length="total"
         @input="updatePagination"
         total-visible="10"
       />
     </v-col>
   </v-row>
 </v-container>
</template>

Opart from the use itself, the component has to be defined in the <script> block:



<script>
 import RecipeGridItem from "../components/RecipeGridItem";

 // ...

 export default {
   components: {RecipeGridItem},
   // ...

The code instantly becomes clearer and easier to maintain, and as I have already mentioned before, it all resembles using predefined functions to display individual components.

 

Graphical interface libraries or: the power of reusable elements!

To finish this chapter, I’d like to underline the role of graphical interface libraries today. Many websites follow the standards defined by the giants. Not so long ago, a set of readymade HTML components called Twitter Bootstrap was very popular. Today, Material Design from Google makes it to the podium in some quarters. Using the most popular, common, and predefined components brings one significant advantage, namely that they have already been tested and you can be quite sure that a given snippet of code will work where it is supposed to (who remembers adapting the HTML and CSS for IE7?). Today, there are not as many discrepancies in interpreting CSS as in the old days. Using a library with numerous functionalities lets us, backend developers, release at least some of the burden of adjusting to what users (bombarded every day with UX/UI experiences) are used to.

In the next and final part about Nuxt.js, we’ll focus on mechanisms that allow you to display your page, i.e. both the UI and the backend data, in multiple languages, and we will add a session and authorisation mechanism.

Article notes

Udostępnij

RST Software Masters

Paweł Grzesiecki

Technical Team Lead

Experienced backend developer at RST Software Masters. Graduate of the Faculty of Information Technology and Management at the Wrocław University of Technology. Staunch believer in the proof of concept methodology and simple but effective solutions. During his 8 years of professional work he’s worked, among others, with NodeJS, PHP, Python, and GoLang. He has realised projects for clients for whom supporting large user traffic was crucial. His pastime interests include physics, cosmology, and the wider history of Europe.

Thank you!

Your email has been sent.

Our website uses cookies to work correctly. Using this website with current settings means that cookies will be stored in the browser’s memory. Cookies settings can be changed in the browser’s options. For more information please visit Cookies policy.