web >> forward

Der Blog für innovative Webtechnologien an der RWU Hochschule Ravensburg-Weingarten

Tutorial: Dive into Vue + Node + REST

This article was updated for Vue – Version 3 – at 27.11.2022

In this tutorial, we will dive into building a single-page-application with Vue, the young and innovative JavaScript frontend framework, and Node, the server-side variant of JavaScript. The architectural paradigm REST (Representational State Transfer), de-facto standard for communication in modern web applications, is used to connect the frontend to the backend.

This video shows what we will build within the next few minutes: a simple app which allows students to rate their professors – or any other service you want to evaluate.

Prerequisites

First of all, ensure that you’ve installed NPM, Node and Vue properly. You can check this by typing the following into your command line interface:

npm -v
node -v
vue --version

Create two folders, one for the backend and one for the frontend code.

Node Backend

We will set up a REST backend based on Node. A simple and straightforward way is to build on the well-known Express framework that allows for building server-side web applications as well as REST-backends. In REST, endpoints are created which are accessible by an URL and a certain HTTP operation, e.g. GET. In our app, the following endpoints will be useful:

EndpointMeaning
GET /profsRetrieve all saved entities
GET /profs/idRetrieve one saved entity
PUT/profs/idModify one entity
DELETE /profs/idDelete one entity
POST /profsCreate a new entity

Let’s start with initializing the backend directory and install some dependencies:

npm init 
npm install express
npm install cors

When executing „npm init“ to create the package.json file, default settings can be kept. The entire code for our backend can then be stored in a file „server.js“:

const express = require("express");
const app = express();
const fs = require("fs");
const cors = require("cors");
const port = 8080;
const filename = __dirname + "/profs.json";

//Middleware
app.use(express.json()); //for parsing application/json
app.use(cors()); //for configuring Cross-Origin Resource Sharing (CORS)
function log(req, res, next) {
    console.log(req.method + " Request at" + req.url);
    next();
}
app.use(log);


//Endpoints
app.get("/profs", function (req, res) {
    fs.readFile(filename, "utf8", function (err, data) {
        res.writeHead(200, {
            "Content-Type": "application/json",
        });
        res.end(data);
    });
});

app.get("/profs/:id", function (req, res) {
    fs.readFile(filename, "utf8", function (err, data) {
        const dataAsObject = JSON.parse(data)[req.params.id];
        res.writeHead(200, {
            "Content-Type": "application/json",
        });
        res.end(JSON.stringify(dataAsObject));
    });
});

app.put("/profs/:id", function (req, res) {
    fs.readFile(filename, "utf8", function (err, data) {
        let dataAsObject = JSON.parse(data);
        dataAsObject[req.params.id].name = req.body.name;
        dataAsObject[req.params.id].rating = req.body.rating;
        fs.writeFile(filename, JSON.stringify(dataAsObject), () => {
            res.writeHead(200, {
                "Content-Type": "application/json",
            });
            res.end(JSON.stringify(dataAsObject));
        });
    });
});

app.delete("/profs/:id", function (req, res) {
    fs.readFile(filename, "utf8", function (err, data) {
        let dataAsObject = JSON.parse(data);
        dataAsObject.splice(req.params.id, 1);
        fs.writeFile(filename, JSON.stringify(dataAsObject), () => {
            res.writeHead(200, {
                "Content-Type": "application/json",
            });
            res.end(JSON.stringify(dataAsObject));
        });
    });
});

app.post("/profs", function (req, res) {
    fs.readFile(filename, "utf8", function (err, data) {
        let dataAsObject = JSON.parse(data);
        dataAsObject.push({
            id: dataAsObject.length,
            name: req.body.name,
            rating: req.body.rating,
        });
        fs.writeFile(filename, JSON.stringify(dataAsObject), () => {
            res.writeHead(200, {
                "Content-Type": "application/json",
            });
            res.end(JSON.stringify(dataAsObject));
        });
    });
});

app.listen(port, () => console.log(`Server listening on port ${port}!`));

For the sake of simplicity, we don’t use a database in this tutorial. Instead, the entities are saved within a file „profs.json“. Create that file and fill it with an empty array: []

Then, your server is ready to be started by typing „node server.js“.

Vue Frontend

Now it’s time to pay attention to the frontend. Change into your frontend directory and create a hello-world-app by typing „npm init vue@latest prof-rating-app“. Again, you can keep the default settings. After a while, the hello-world-application and all required dependencies should have been installed.

Since you want to use five stars to rate me and my colleagues, we make use of the v-rating component of the plugin Vuetify. Therefore, install vuetify by changing into the app’s directory and typing „npm add vuetify@^3.0.1“. To install Material Design icon fonts, type „npm install @mdi/font -D“. Other icon fonts can be installed as described here.

For communicating with the backend via REST, we use Axios. Install the library by typing „npm install axios“.

In the hello-world-code that has been created you will find main.js as an entry point of the application which hosts the one and only Vue instance. It renders App.vue and binds it to the html element with the id „app“. For adding Vuetify to the project, change the file content to:

import { createApp } from 'vue'
import App from './App.vue'

// import './assets/main.css'

// Vuetify
import 'vuetify/styles'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { createVuetify } from 'vuetify'
import '@mdi/font/css/materialdesignicons.css' // Ensure you are using css-loader
import { aliases, mdi } from 'vuetify/iconsets/mdi'

const vuetify = createVuetify({
    components,
    directives,
    icons: {
        defaultSet: 'mdi',
        aliases,
        sets: {
          mdi,
        }
      },
})

createApp(App).use(vuetify).mount('#app')

In the project folder, you’ll find the index.html that includes the div-item with the id „app“. During development, this file does not need to be modified.

If you take a look at App.vue, you’ll notice this component already makes use of two child-components „HelloWorld“ and „TheWelcome“. Components are one of the fundamental aspects of JavaScript-frameworks to encapsulate and reuse code.

We also need some components in our web application. Let’s start with a component that contains the input, the rating-element and the button to save the data. Create a file „AddEntry.vue“ and save it into the „components“ folder. The component’s code is as follows:

<template>
  <div>
    <v-text-field id="input" label="Name of professor" v-model="name" size="100%"></v-text-field>
    <v-rating id="rating" v-model="rating" background-color="black" color="#ffcc00"></v-rating>
    <v-btn @click="addEntry">Add</v-btn>
  </div>
</template>

<script>
export default {
  name: "AddEntry",
  data: function() {
    return {
      name: "",
      rating: 0
    };
  },
  methods: {
    addEntry: function() {
      if (this.name.length > 0) {
        this.$emit("entryAdded", {
          name: this.name,
          rating: this.rating
        });
        this.name = "";
        this.rating = 0;
      }
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
div {
  display: flex;
  align-items: center;
}
</style>

Then, a second component will be created which lists the saved elements. Create a file „ListEntries.vue“ within the components folder and save the following content:

<template>
  <div>
    <span v-show="!isEditable" id="name" @click="editEntry">{{ entry.name }}</span>
    <v-text-field
      id="input"
      v-show="isEditable"
      label="Name of professor"
      v-model="entry.name"
      @focusout="editEntry"
      ref="input"
    ></v-text-field>

    <v-rating v-model="entry.rating" background-color="black" color="#ffcc00" @input="editRating"></v-rating>
    <v-btn @click="removeEntry">Remove</v-btn>
  </div>
</template>

<script>
export default {
  name: "ListEntries",
  props: ["entry", "index"],
  data: function() {
    return {
      isEditable: false
    };
  },
  methods: {
    removeEntry: function() {
      this.$emit("entryRemoved", {
        index: this.index
      });
    },
    editRating: function() {
      this.$emit("entryEdited", {
        index: this.index,
        name: this.entry.name,
        rating: this.entry.rating
      });
    },
    editEntry: function() {
      if (this.isEditable) {
        this.isEditable = false;
        this.$emit("entryEdited", {
          index: this.index,
          name: this.entry.name,
          rating: this.entry.rating
        });
      } else {
        this.isEditable = true;

        // Focus the component, but we have to wait
        // so that it will be showing first.
        this.$nextTick().then(() => {
          this.focusInput();
        });
      }
    },
    focusInput() {
      this.$refs.input.focus();
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
div {
  display: flex;
  align-items: center;
}
#name,
#input {
  width: 300px;
  text-align: left;
}
</style>

For using the two child components, we need to import them into the parent component „App.vue“. Therefore, replace the existing „App.vue“-code with the following:

<template>
  <div>
    <h1>Professor Rating App</h1>
    <AddEntry id="addEntry" @entryAdded="addEntry"></AddEntry>
    <ListEntries
      id="listEntry"
      v-for="(singleEntry, index) of listOfEntries"
      :key="index"
      :entry="singleEntry"
      :index="index"
      @entryRemoved="removeEntry"
      @entryEdited="editEntry"
    ></ListEntries>
  </div>
</template>

<script>
import AddEntry from "./components/AddEntry.vue";
import ListEntries from "./components/ListEntries.vue";
import axios from "axios";

export default {
  name: "App",
  components: {
    AddEntry,
    ListEntries
  },
  data: function() {
    return {
      listOfEntries: []
    };
  },
  methods: {
    addEntry: function(e) {
      axios
        .post("http://localhost:8080/profs/", {
          name: e.name,
          rating: e.rating
        })
        .then(response => {
          this.listOfEntries = response.data;
        });
    },
    editEntry: function(e) {
      axios
        .put("http://localhost:8080/profs/" + e.index, {
          name: e.name,
          rating: e.rating
        })
        .then(response => {
          this.listOfEntries = response.data; //TODO: change this, do not return full list
        });
    },
    removeEntry: function(e) {
      axios.delete("http://localhost:8080/profs/" + e.index).then(response => {
        this.listOfEntries = response.data;
      });
    }
  },
  mounted() {
    axios.get("http://localhost:8080/profs/").then(response => {
      this.listOfEntries = response.data;
    });
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  padding: 60px;
  width: 700px;
  margin-left: auto;
  margin-right: auto;
  background-color: lightblue;
}
#addEntry,
h1 {
  margin-bottom: 40px;
}
</style>

Now you can start the development server by typing „npm run dev“. If everything went alright, you will see the app in your browser at the local URL displayed in your command line interface, e.g. http://127.0.0.1:5174/. If the backend server is successfully started, the list of profs can be extended, modified or shortened.

This was easy, wasn’t it? Take your time to read the code more in detail to understand how the basic CRUD operations (create/read/update/delete) are performed. Hopefully I could motivate for becoming a new web developer, thanks for reading this tutorial!

« »

© 2024 web >> forward | Theme von Anders Norén | Impressum