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:
Endpoint | Meaning | ||
GET /profs | Retrieve all saved entities | ||
GET /profs/id | Retrieve one saved entity | ||
PUT/profs/id | Modify one entity | ||
DELETE /profs/id | Delete one entity | ||
POST /profs | Create 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!
Malte
5. Juni 2020 — 10:07
I found an errror in the server.js-code. When deleting a specific prof, the IDs aren’t updated. Thus, when having the IDs 0 and 1, deleting the professor with the ID 0 and adding another professor leaves two professors with the ID 1.
Also the IDs of the professors that are stored after the delted professor have faulty IDs, since:
1. ID 1
2. ID 2
3. ID 3
Now Delete 2.
1. ID1
2. ID 3
Howver, on GET /profs/3 (for professor with ID 3), noting is returned and for GET /profs/2, the prof with the ID 3 is returned.
Thus the IDs should be updated after deleting. My solution was interted after line 55:
for(let i = 0; i < dataAsObject.length; i++){
dataAsObject[i].id = i;
}
Marius Hofmeister
29. Juli 2020 — 18:05
Thanks a lot for your comment!
I really appreciate your improvement.