Build an isomorphic application with Nuxt.js and Node
Tags: nuxt, node, javascript, webdev
Published: 2020-02-05 17:50:07

---
title: Build an isomorphic application with Nuxt.js and Node
published: true
date: 2020-02-03 17:00:56 UTC
tags: nuxt,node,javascript,webdev
canonical_url: https://blog.logrocket.com/build-an-isomorphic-application-with-nuxt-js-and-node/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/mwom1qgqv7a1kcc3wm9t.png
---
**Written by [Jordan Irabor](https://blog.logrocket.com/author/jordanirabor/)**✏️

## Introduction

Single-page applications (SPAs) transformed the way internet users interact with web applications. A SPA is an application that improves user experience by dynamically updating the content of a single page, rather than fetching every new page from a server. These kind of web applications offer the following benefits:

### Pleasant routing

There is no page reload as users move from one page to another and this can give the feel of a native application rather than a web application. Some developers add transition effects on each navigation to give an even smoother experience.

### Consumes less bandwidth

SPAs do not have to fetch entire page documents from a server after the main JavaScript bundle has loaded. This reduces the bandwidth used in data exchange and makes the web applications easy to use with slow internet connections.

### Fast load time

In traditional web applications, the browser sends a request to the server for an HTML file on each page navigation. SPAs only send this request once, on the first load. Any other data needed will be dynamically retrieved and injected. This makes SPAs faster than regular websites as they do not have to load new pages when users navigate the application.

While the concept of a SPA is shiny and packed with a lot of advantages, it also introduces a few disadvantages because of its design. Some of these disadvantages are:

- The initial page load time is usually slow because the JavaScript bundle needed to run the application dynamically is heavy
- Because the web application is client-side rendered, some search engine web crawlers and social network robots do not see the content for the application when they crawl the pages

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## What are isomorphic applications?

Isomorphic applications, as described [here](https://en.wikipedia.org/wiki/Isomorphic_JavaScript), were designed to solve the problems discussed above:

> Isomorphic applications, also known as Universal JavaScript applications are JavaScript applications that preload data using server-side rendering before running the application on the client-side. This ensures that all content is available for crawlers and other bots to index.

Setting up a server-side rendered JavaScript application from scratch can be a hassle as a lot of configuration is required. This is the problem Nuxt aims to solve for Vue developers, the [official Nuxt](https://nuxtjs.org/guide/) website describes it as:

> Nuxt is a progressive framework based on Vue, it is used to create modern server-side rendered web applications. You can use Nuxt as a framework to handle all the UI rendering of your application while preloading its data on the server-side.

This schema shows what happens under the hood, in a Nuxt application, when the server is called or when the user navigates through a Nuxt application:

![nuxt-schema](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/01/nuxtappschema.png?resize=460%2C600&ssl=1)<figcaption id="caption-attachment-13403">nuxt-schema</figcaption>

In this article, we will build an isomorphic pet adoption website using Nuxt and Node. Here’s a demo of how the final application will work:

![node and nuxt app](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/01/petadoptionsite.gif?resize=730%2C500&ssl=1)

Let’s get started.

## Prerequisites

You’ll need the following for this tutorial:

- [MongoDB](https://www.mongodb.com/)
- [Node](https://nodejs.org/en/download/)
- [Npm](https://www.npmjs.com/)
- A code editor of choice
- Knowledge of Node and Vue

For reference, the source code for this tutorial is available on [GitHub](https://github.com/Jordanirabor/isomorphic-application).

## Building the backend

We will separate the backend code from the frontend code by putting them in two different folders, but first, let’s create a parent directory to house the entire project:

```jsx
$ mkdir isomorphic-application
$ cd isomorphic-application
```

Let’s create the `backend` folder within the project directory:

```jsx
$ mkdir backend
$ cd backend
```

The first thing we want to do is to initialize a new npm project:

```jsx
$ npm init -y
```

Let’s install [Nodemon](https://www.npmjs.com/package/nodemon) to help us automatically refresh our server when we make code changes:

```jsx
$ npm install nodemon -g
```

We need these other dependencies to help us build the server, parse data, handle images, and log incoming requests:

```jsx
$ npm install express cors request body-parser multer morgan mongoose crypto --save
```

Let’s create the following folder structure in the `backend` directory:

```jsx
backend
└── /models
    └── pet.js
└── /routes
    └── api.js
└── index.js
└── mock.js
```

> Note that there will be other files and folders (like `node_modules`) automatically generated in the backend directory.

Let’s start updating these files one by one to gradually become our backend server to handle and process requests. Paste in the following code in the `models/pet.js` file:

```jsx
// models/pet.js

const mongoose = require('mongoose');

const Schema = mongoose.Schema;

const petSchema = new Schema({
    name: { type: String },
    type: { type: String },
    imageUrl: { type: String },
    description: { type: String }
})

module.exports = mongoose.model('Pet', petSchema);
```

In the snippet above, we defined the schema for the pets we wanted to create and exported it as a [Mongoose](https://www.npmjs.com/package/mongoose) model. We want each pet to have the following fields:

1. name
2. type (maybe a cat or a dog)
3. imageUrl (the address of its image)
4. description

Now paste in the following code in the `routes/api.js` file:

```jsx
// routes/api.js

const Pet = require('../models/pet');
const express = require('express');
const path = require('path')
const multer = require('multer')
const crypto = require('crypto')
const router = express.Router();

const storage = multer.diskStorage({
    destination: 'public',
    filename: (req, file, callback) => {
        crypto.pseudoRandomBytes(16, function (err, raw) {
            if (err) return callback(err);
            callback(null, raw.toString('hex') + path.extname(file.originalname));
        });
    }
});

let upload = multer({ storage: storage })

router.post('/pet/new', upload.single('image'), (req, res) => {
    if (!req.file) {
        console.log("Please include a pet image");
        return res.send({
            success: false
        });
    } else {
        const host = req.get('host')
        const imageUrl = req.protocol + "://" + host + '/' + req.file.path;
        Pet.create({
            name: req.body.name,
            type: req.body.type,
            description: req.body.description,
            imageUrl
        }, (err, pet) => {
            if (err) {
                console.log('CREATE error: ' + err);
                res.status(500).send('Error')
            } else {
                res.status(200).json(pet)
            }
        })
    }
})

router.get('/pet/:_id', (req, res) => {
    Pet.findById(req.params._id, (err, pet) => {
        if (err) {
            console.log('RETRIEVE error: ' + err);
            res.status(500).send('Error');
        } else if (pet) {
            res.status(200).json(pet)
        } else {
            res.status(404).send('Item not found')
        }
    })
})

router.get('/pets', (req, res) => {
    const pets = Pet.find({}, (err, pets) => {
        if (err) {
            console.log('RETRIEVE error: ' + err);
            res.status(500).send('Error');
        } else if (pets) {
            res.status(200).json(pets);
        }
    })
})

module.exports = router;
```

In the snippet above, we imported the [Multer](https://www.npmjs.com/package/multer) package and used it to define the destination for images on our local machine. We also used the [Crypto](https://www.npmjs.com/package/crypto) package to generate a new random name for the images of pets that will be uploaded.

We used the [Express](https://www.npmjs.com/package/express) router framework to create three routes:

1. `/pet/new` handles the upload of new pet objects
2. `/pet/:_id` finds and returns an existing pet to be rendered on the client-side
3. `/pets` returns all pets

Finally, at the bottom of the snippet, we exported the router.

Open the `backend/index.js` file and paste in the following snippet:

```jsx
// backend/index.js

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose')
const morgan = require('morgan');
const api = require('./routes/api')
const pets = require('./mock')
const path = require('path');
const app = express()

app.use((req, res, next) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
    next();
})

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use('/api', api);
app.use(morgan('dev'));
app.use('/public', express.static(path.join(__dirname, 'public')));

mongoose.connect('mongodb://localhost:27017/pets', { useNewUrlParser: true });

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'Connection Error'))
db.once('open', () => {
    app.listen(9000, () => {
        console.log('Running on port 9000')
    })
    const petCollection = db.collection('pets')
    petCollection.estimatedDocumentCount((err, count) => {
        if (count) return
        petCollection.insertMany(pets)
    })
})
```

In the code above, we imported the dependencies we need (including a mock file that we’ve yet to create) and set headers to prevent CORS issues since the client-side application will run on a different port.

We registered the `/public` (our destination for images created by Multer) as a [static](https://expressjs.com/en/starter/static-files.html) URL and connected to MongoDB using the mongoose client. With this block of code below, we start the server on port `9000` and seed the database using the mock data if it is empty:

```jsx
db.once('open', () => {
    app.listen(9000, () => {
        console.log('Running on port 9000')
    })
    const petCollection = db.collection('pets')
    petCollection.estimatedDocumentCount((err, count) => {
        if (count) return
        petCollection.insertMany(pets)
    })
})
```

Let’s create the mock data now, paste the following code in the `backend/mock.js` file:

```jsx
// backend/mock.js

const pets = [{
    'name': 'Calvin',
    'type': 'Dog',
    'imageUrl': 'https://placedog.net/636/660',
    'description': 'Great at giving warm hugs.'
},
{
    'name': 'Carly',
    'type': 'Dog',
    'imageUrl': 'https://placedog.net/660/636',
    'description': 'Has a little nice tail'
},
{
    'name': 'Muffy',
    'type': 'Cat',
    'imageUrl': 'https://placekitten.com/636/660',
    'description': 'Loves drinking milk'
},
{
    'name': 'Beth',
    'type': 'Cat',
    'imageUrl': 'https://placekitten.com/660/636',
    'description': 'Might give gentle bites when played with'
}]

module.exports = pets
```

The snippet above is just dummy for the database because we want the application to always have some pets to display, even on the first run.

We can start the backend by running the following command in the `backend` directory:

```jsx
$ node index.js
```

To test the backend at this stage, you can use a REST client (like [PostMan](https://www.getpostman.com/)) to make requests to the endpoints.

## Building the frontend

An easy way to create a Nuxt project is to use the template created by the team. We will install it into a folder called `frontend` as we mentioned before, so run the following command:

```jsx
$ vue init nuxt/starter frontend
```

> Note: You need vue-cli to run the command above. If you don’t have it installed on your computer, you can run `npm install -g @vue/cli` to install it.

Once the command runs, you will be met with a prompt asking some questions. You can press the `Return` key to accept the default values as they will work just fine for this project. Now run the following commands:

```jsx
$ cd frontend
$ npm install
```

We will start the development server with this command:

```jsx
$ npm run dev
```

The server will start on the address [http://localhost:3000](http://localhost:3000) and you will see the nuxt template starter page:

![nuxt project](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/01/frontendnextproject.gif?resize=730%2C582&ssl=1)  
To confirm its server-side rendering, you can view the page’s source on your browser and you will see that the content on the page is rendered on the server and not injected during run-time by client-side JavaScript.

Let’s make a few configurations by updating the `nuxt.config.js` file accordingly:

```jsx
// ./nuxt.config.js

module.exports = {
  /*
   * Headers of the page
   */
  head: {
    titleTemplate: '%s | Adopt a pet today',
    // ...
    link: [
      // ...
      {
        rel: 'stylesheet',
        href: 'https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css'
      },
      { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300&display=swap' }
    ]
  },
  // ...
}
```

We just configured our project to dynamically update its title depending on the page we are on using the `titleTemplate` option. We will inject the titles dynamically by setting the `title` property on each page and layout in our application and the `%s` placeholder will be updated.

We also pulled in [Bulma CSS](https://bulma.io/) to style our application using the `link` property.

It is worth mentioning that Nuxt uses [vue-meta](https://github.com/nuxt/vue-meta) to update the headers of our application as we navigate through.

## Extend the default layout

The Nuxt template we installed ships with a default layout. We will customize this layout and use it to serve all the pages and components we define for this application. Let’s replace the content of the `layouts/default.vue` file with the snippet below:

```jsx
<!-- ./layouts/default.vue -->

<template>
  <div>
    <!-- begin navigation -->
    <nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
      <div class="container">
        <div class="navbar-start">
          <nuxt-link to="/" class="navbar-item is-half">
            <img
              src="https://www.graphicsprings.com/filestorage/stencils/f6e5c06cad423f0f7e6cae51c7a41f37.svg"
              alt="Logo: an image of a doggy biting a juicy bone!"
              width="112"
              height="28"
            />
          </nuxt-link>
          <nuxt-link active-class="is-active" to="/" class="navbar-item is-tab" exact>Home</nuxt-link>
          <nuxt-link
            active-class="is-active"
            to="/pet/new"
            class="navbar-item is-tab"
            exact
          >Post your own pet 😎</nuxt-link>
        </div>
      </div>
    </nav>
    <!-- end navigation -->
    <!-- displays the page component -->
    <nuxt />
    <!-- begin footer -->
    <footer class="footer home-footer has-background-black">
      <div class="content has-text-centered">
        <p class="has-text-white">
          <strong class="has-text-white">Pet adoption website</strong> by
          <a href="https://github.com/Jordanirabor">Jordan</a>
        </p>
      </div>
    </footer>
    <!-- end footer -->
  </div>
</template>

<style>
.main-content {
  margin: 20px 0;
}
body {
  font-family: "Open Sans Condensed", sans-serif;
}
p {
  font-size: 22px;
}
.home-footer{
  margin-top: 20vh;
}
</style>
```

In the custom layout above, we added a navigation header and used the `<nuxt-link>` to generate links to the pages we want to be able to route to:

1. `/` routes to the homepage
2. `/pet/new` routes to the page that allows users to upload new pets

The single `<nuxt>` component is responsible for rendering dynamic page content.

## Creating the homepage

Nuxt makes routing easy for us by giving us the option of creating pages by adding [single file components](https://vuejs.org/v2/guide/single-file-components.html) in the pages directory. In other words, every file in the `pages` directory becomes a route that can be visited.

Let’s create the homepage by replacing the code in the `pages/index.vue` file with the following snippet:

```jsx
<!-- ./pages/index.vue -->

<template>
  <div>
    <section class="hero is-medium is-dark is-bold">
      <div class="hero-body">
        <div class="container">
          <h1 class="title">Adopt a new pet today!</h1>
          <h2
            class="subtitle"
          >You just might need a curious kitten to stare at you as you slap the keyboard tirelessly 😃</h2>
        </div>
      </div>
    </section>
  </div>
</template>

<script>
export default {
  head: {
    title: "Home"
  }
};
</script>
```

In the snippet above, we defined some markup using [Bulma CSS](https://bulma.io/) classes. In the script section, we specified a `title` to equal “Home” so that the `titleTemplate` we configured is updated before the page is rendered on the client-side.

We can start the development server (if it isn’t already running). Take a look at what the homepage currently looks like:

![](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/01/uipetadopt-1.png?resize=730%2C495&ssl=1)  
This looks good, now we want to fetch the available pets from the backend server, loop through them and display each one of them in the homepage. Let’s start by replacing the `<template>` of the `pages/index.vue` file with this updated version:

```jsx
<!-- ./pages/index.vue -->

<template>
  <!-- begin header -->
  <div>
    <section class="hero is-medium is-dark is-bold">
      <div class="hero-body">
        <div class="container">
          <h1 class="title">Adopt a new pet today!</h1>
          <h2
            class="subtitle"
          >You just might need a curious kitten to stare at you as you slap the keyboard tirelessly 😃</h2>
        </div>
      </div>
    </section>
    <!-- end header -->
    <!-- begin main content -->
    <section class="main-content">
      <div class="container">
        <h1 class="title has-text-centered">Available pets</h1>
        <div class="columns is-multiline">
          <div class="column is-half" v-for="pet in pets" :key="pet._id">
            <div class="card">
              <header class="card-header">
                <p class="card-header-title is-centered">{{ pet.name }}</p>
              </header>
              <div class="card-content">
                <figure class="image is-3by2">
                  <img :src="`${pet.imageUrl}`" />
                </figure>
              </div>
              <footer class="card-footer">
                <nuxt-link :to="`/pet/${pet._id}`" class="card-footer-item">
                  <button class="button is-dark">Learn more about {{ pet.name }}</button>
                </nuxt-link>
              </footer>
            </div>
          </div>
        </div>
      </div>
    </section>
    <!-- end main content -->
  </div>
</template>
```

We will also update the `<script>` section so it makes a request to the backend server and loads the pets data object before rendering the client-side:

```jsx
<!-- ./pages/index.vue -->

<script>
export default {
  head: {
    title: "Home"
  },
  async asyncData(context) {
    try {
      return await fetch("http://localhost:9000/api/pets")
        .then(res => res.json())
        .then(data => {
          return { pets: data };
        });
    } catch (e) {
      console.error("SOMETHING WENT WRONG :" + e);
    }
  },
  data() {
    return {
      pets: []
    };
  }
};
</script>
```

In the code above, we used the `asyncData` method to fetch the `pets` data (using the promise based [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)) from the backend server. We use this method because it fetches data and renders it on the server-side before sending a response to the browser. After its successful retrieval of data from the backend server, the `pets` data object becomes accessible as a data property on the Vue object.

Now we can revisit our application and see the homepage pre-populated with our mock data from the backend server:

> ![](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/01/petadoptionsite2.gif?resize=730%2C506&ssl=1)  
> Note: remember to keep the Node backend server running so the mock data is available.

## Build the dynamic single pet page

We want to be able to click on the button attached to each pet’s card component and be routed to a page that displays more information of that particular pet. How do we achieve this with Nuxt? Nuxt lets us add dynamic routes and we can access them with a URL like this: `/pet/1`.

To achieve this, we need to create a new directory in the pages folder called `pet`. We will then structure it like this:

```jsx
pages
└── pet
    └── _id
        └── index.vue
```

Structuring the directory hierarchy like this has the effect of generating dynamic routes with the following configuration:

```jsx
router: {
  routes: [
    // ...
    {
      name: 'pet-id',
      path: '/pet/:id',
      component: 'pages/pet/_id/index.vue'
    }
  ]
}
```

Once the directory structure has been achieved, paste in the following code in the `pages/pet/_id/index.vue` file:

```jsx
<!-- ./pages/pet/_id/index.vue -->

<template>
  <div class="main-content">
    <div class="container">
      <div class="card">
        <header class="card-header">
          <p class="card-header-title is-centered">{{ pet.name }}</p>
        </header>
        <div class="card-content has-background-dark">
          <figure class="image is-1by1">
            <img class :src="`${pet.imageUrl}`" />
          </figure>
        </div>
        <br />
        <h4 class="title is-5 is-marginless">
          <p class="has-text-centered">About</p>
          <hr />
          <p class="has-text-centered">
            <strong>{{ pet.description }}</strong>
          </p>
          <br />
        </h4>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  validate({ params }) {
    return /^[a-f\d]{24}$/i.test(params.id);
  },
  async asyncData({ params }) {
    try {
      let pet = await fetch(`http://localhost:9000/api/pet/${params.id}`)
        .then(res => res.json())
        .then(data => data);
      return { pet };
    } catch (e) {
      console.error("SOMETHING WENT WRONG :" + e);
      return { pet: {} };
    }
  },
  head() {
    return {
      title: this.pet.name,
      meta: [
        {
          hid: "description",
          name: "description",
          content: this.pet.description
        }
      ]
    };
  }
};
</script>
```

In the `<script>` section above, we used a new method called `validate()`. We used this method to check that the route parameter passed is a valid Hexadecimal MongoDB [ObjectId](https://docs.mongodb.com/manual/reference/method/ObjectId/). In the case where the check fails, Nuxt will automatically [reload the page as a 404 error](https://nuxtjs.org/api/pages-validate).

We also used `asyncData` here to fetch the single pet object before rendering the page. On visiting our application again, it will look like this:

![](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/01/carlypetadoptui.gif?resize=730%2C506&ssl=1)

## Uploading your pet

At this stage, it’s already fun to browse our application and see cute pet pictures, but what if we had a pet we want to put up for adoption? Let’s create a new file — `pages/pet/new.vue` — to implement this feature. Paste in the following code in the `pages/pet/new.vue` file:

```jsx
<!-- pages/pet/new.vue -->  

<template>
  <div class="container">
    <br />
    <h1 class="title has-text-centered">{{pet.name}}</h1>
    <div class="columns is-multiline">
      <div class="column is-half">
        <form @submit.prevent="uploadPet">
          <div class="field">
            <label class="label">Name</label>
            <div class="control">
              <input
                class="input"
                type="text"
                placeholder="What is your pet's name?"
                v-model="pet.name"
              />
            </div>
          </div>
          <div class="field">
            <label class="label">Description</label>
            <div class="control">
              <textarea
                class="textarea"
                v-model="pet.description"
                placeholder="Describe your pet succintly"
              ></textarea>
            </div>
          </div>
          <div class="file">
            <label class="file-label">
              <input class="file-input" @change="onFileChange" type="file" name="resume" />
              <span class="file-cta">
                <span class="file-icon">
                  <i class="fas fa-upload"></i>
                </span>
                <span class="file-label">Upload a pet image…</span>
              </span>
            </label>
          </div>
          <br />
          <div class="field">
            <label class="label">Type of pet</label>
            <div class="control">
              <div class="select">
                <select v-model="pet.type">
                  <option value="Cat">Cat</option>
                  <option value="Dog">Dog</option>
                </select>
              </div>
            </div>
          </div>
          <div class="field is-grouped">
            <div class="control">
              <button class="button is-link">Submit</button>
            </div>
          </div>
        </form>
      </div>
      <div class="column is-half">
        <figure v-if="preview" class="image container is-256x256">
          <img
            style="border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
            :src="preview"
            alt
          />
        </figure>
        <figure v-else class="image container is-256x256">
          <img
            style="border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
            src="https://via.placeholder.com/150"
          />
        </figure>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  head() {
    return {
      title: "New Pet"
    };
  },
  data() {
    return {
      pet: {
        name: "",
        image: "",
        description: "",
        type: "Cat"
      },
      preview: ""
    };
  },
  methods: {
    onFileChange(e) {
      let files = e.target.files || e.dataTransfer.files;
      if (!files.length) {
        return;
      }
      this.pet.image = files[0];
      this.createImage(files[0]);
    },
    createImage(file) {
      let reader = new FileReader();
      let vm = this;
      reader.onload = e => {
        vm.preview = e.target.result;
      };
      reader.readAsDataURL(file);
    },
    async uploadPet() {
      let formData = new FormData();
      for (let data in this.pet) {
        formData.append(data, this.pet[data]);
      }
      try {
        let response = await fetch("http://localhost:9000/api/pet/new", {
          method: "post",
          body: formData
        });
        this.$router.push("/");
      } catch (e) {
        console.error(e);
      }
    }
  }
};
</script>
```

In the code above, the `uploadPet()` method is an asynchronous method that posts a new pet object to the backend server and redirects back to the homepage on successful upload:

![](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/01/successfulpetprofuleupload-1.gif?resize=730%2C500&ssl=1)

Hurray! This brings us to the end of the tutorial.

## Conclusion

In this article, we learned about SPAs, their advantages and disadvantages. We also explored the concept of isomorphic applications and used Nuxt to build a pet adoption website that preloads data on the server-side before rendering the UI.

The source code for this tutorial is available on [GitHub](https://github.com/Jordanirabor/isomorphic-application).

* * *
## 200's only ‎✅: Monitor failed and show GraphQL requests in production 

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, [try LogRocket.](https://www2.logrocket.com/signup-lr)

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/jd2amkhuiynfjf3i33qk.png)

[LogRocket](https://www2.logrocket.com/signup-lr) is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. [Start monitoring for free.](https://www2.logrocket.com/signup-lr)
* * *

The post [Build an isomorphic application with Nuxt.js and Node](https://blog.logrocket.com/build-an-isomorphic-application-with-nuxt-js-and-node/) appeared first on [LogRocket Blog](https://blog.logrocket.com).