Create a Full Stack Web App with the MEVN Stack
Tags: webdev, javascript, node, vue
Published: 2021-01-25 01:25:38

**Check out my books on Amazon at https://www.amazon.com/John-Au-Yeung/e/B08FT5NT62**

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

The MEVN stack is a set of technologies for full-stack apps.

MEVN stands for MongoDB, Express, Vue.js, and Node.js

In this article, we’ll take a look at how to create a simple todo app with authentication with the MEVN stack.

### Setting Up the Project

The first step to create a full-stack MEVN app is to set up the project.

First, we create a project folder, then add the `backend` and `frontend` folders inside it.

`backend` has the Express app.

`frontend` has the Vue.js app. We’ll use Vue 3 for the front end.

Next, to create our Express app, we run the Express Generator to create the files.

To do this, go into the `backend` and run:

```
npx express-generator
```

We may need admin privileges to do this.

Next, we go into the `frontend` folder.

We install the latest version of the Vue CLI by running:

```
npm install -g @vue/cli
```

Vue CLI 4.5 or later can create Vue 3 projects.

Then in the `frontend` folder, we run:

```
vue create .
```

Then we choose the ‘Vue 3 (default)’ option.

Next, we have to install the packages.

We stay in the `frontend` folder and run:

```
npm i axios vue-router@4.0.0-beta.12
```

to install the Axios HTTP client and Vue Router 4.x, which is compatible with Vue 3.

In the `backend` folder, we run:

```
npm i cors momgoose jsonwebtoken
```

to install the CORS and Mongoose packages to enable cross-domain communication and work with MongoDB.

`jsonwebtoken` lets us add JSON web token authentication into our app.

### Create the Back End

Now we’re ready to create the back end app to work with MongoDB and add authentication.

First, we create `db.js` in the backend folder and add:

```
const { Schema, createConnection } = require('mongoose');
const connection = createConnection('mongodb://localhost:27017/mevn-example', { useNewUrlParser: true });

const userSchema = new Schema({
  name: String,
  password: String
});

const User = connection.model('User', userSchema);

const todoSchema = new Schema({
  name: String,
  done: Boolean,
  user: { type: Schema.Types.ObjectId, ref: 'User' },
});

const Todo = connection.model('Todo', todoSchema);

module.exports = {
  User,
  Todo
}
```

We connect to the MongoDB database with Mongoose and create the models.

To create the models, we use the `Schema` constructor to create the schema.

We add the properties and their data types.

The `user` property references the Object ID from the `User` model so that we can link the todo entry to the user.

Then we call `connection.model` to use the schema to create the model.

Then, we create `constants.js` in the `backend` folder and write:

```
module.exports = {
  SECRET: 'secret'
}
```

We put the secret for the JSON web token into this file and use them in the route files.

Next, we go into the `routes` folder and add the files for the API routes.

We create `todos.js` in the `routes` folder and write:

```
var express = require('express');
var router = express.Router();
const { Todo } = require('../db');
const jwt = require('jsonwebtoken');
const { SECRET } = require('../constants');

const verifyToken = (req, res, next) => {
  try {
    req.user = jwt.verify(req.headers.authorization, SECRET);
    return next();
  } catch (err) {
    console.log(err)
    return res.status(401);
  }
}

router.get('/', verifyToken, async (req, res) => {
  const { _id } = req.user;
  const todos = await Todo.find({ user: _id })
  res.json(todos);
});

router.get('/:id', verifyToken, async (req, res) => {
  const { _id } = req.user;
  const { id } = req.params;
  const todo = await Todo.findOne({ _id: id, user: _id })
  res.json(todo);
});

router.post('/', verifyToken, async (req, res) => {
  const { name } = req.body;
  const { _id } = req.user;
  const todo = new Todo({ name, done: false, user: _id })
  await todo.save()
  res.json(todo);
});

router.put('/:id', verifyToken, async (req, res) => {
  const { name, done } = req.body;
  const { id } = req.params;
  const todo = await Todo.findOneAndUpdate({ _id: id }, { name, done })
  await todo.save();
  res.json(todo);
});

router.delete('/:id', verifyToken, async (req, res) => {
  const { id } = req.params;
  await Todo.deleteOne({ _id: id })
  res.status(200).send();
});

module.exports = router;
```

The `verifyToken` middleware has the `jwt.verify` method to verify the token with our `SECRET` key.

If it’s valid, we set the `req.user` to the decoded token.

Then we call `next` to call the router handlers.

Then we have the GET `/` route to get the user data from `req.user` and then call `Todo.find` to find the todo entries for the user.

`Todo` is the model we created earlier.

The GET `/:id` route gets the `Todo` entry by the ID.

`findOne` gets the first result in the `todos` collection.

`req.params` gets the URL parameters from the URL parameter.

Then POST `/` route gets the `name` from the `req.body` property, which has the request body daya.

And we get the `_id` property from the `req.user` property to get the user data.

We use both to create the todo entry.

`user` has the Object ID for the user.

The PUT `/:id` route is used to update the user.

We get the `name` and `done` from `req.body` , which has the request body.

`req.params` has the `id` property which has the ID of the todo entry.

We use the `id` to find the todo entry.

And update the `name` and `done` property to update the entry.

Then we call `todo.save()` to save the data.

The DELETE `/:id` route lets us delete a todo entry by its ID.

We call `deleteOne` with the `_id` to do that.

The `verifyToken` will make sure the token is valid before we run the route handlers.

`req.headers.authorization` has the auth token. `req.headers` has the HTTP request headers.

Next, we create `users.js` in the `routes` folder and write:

```
var express = require('express');
var router = express.Router();
const bcrypt = require('bcrypt');
const { User } = require('../db');
const { SECRET } = require('../constants');
const jwt = require('jsonwebtoken');
const saltRounds = 10;

router.post('/register', async (req, res) => {
  const { name, password } = req.body;
  const existingUser = await User.findOne({ name });
  if (existingUser) {
    return res.json({ err: 'user already exists' }).status(401);
  }
  const hashedPassword = await bcrypt.hash(password, saltRounds);
  const user = new User({
    name,
    password: hashedPassword
  })
  await user.save();
  res.json(user).status(201);
});

router.post('/login', async (req, res) => {
  const { name, password } = req.body;
  const { _id, password: userPassword } = await User.findOne({ name });
  const match = await bcrypt.compare(password, userPassword);
  if (match) {
    const token = await jwt.sign({ name, _id }, SECRET);
    return res.json({ token });
  }
  res.status(401);
});

module.exports = router;
```

It’s similar to the `todos.js` .

We have the POST `/register` route to get the `name` and `password` properties from the JSON request body.

Then we check if the user with the given name exists.

Then we get the `bcrypt.hash` to hash the password with the secret.

And then we save the user with the `name` and then `hashedPassword`.

The POST `/login` route gets the `name` and `password` from the JSON request body.

We use the `bcrypt.compare` method to compare the password.

Then we create the token and return it in the response if validation is successful.

Otherwise, we send a 401 response.

Finally, we bring everything together in `app.js` in the `backend` folder.

We replace what’s there with:

```
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var cors = require('cors')

var todorouter = require('./routes/todo');
var usersRouter = require('./routes/users');

var app = express();
app.use(cors())
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/todos', todorouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;
```

We add the `cors` middleware so that we can communicate with the front end.

We have the `todorouter` and `usersRouter` to add the routes to our app.

### Vue 3 Front End

Now that we finished the back end, we work on the front end.

First, in the `components` folder, we create `TodoForm.vue` and write:

```
<template>
  <div>
    <h1>{{ edit ? "Edit" : "Add" }} Todo</h1>
    <form @submit.prevent="submit">
      <div class="form-field">
        <label>Name</label>
        <br />
        <input v-model="form.name" />
      </div>
      <div>
        <label>Done</label>
        <input type="checkbox" v-model="form.done" />
      </div>
      <div>
        <input type="submit" value="Submit" />
      </div>
    </form>
  </div>
</template>

<script>
import axios from "axios";
import { APIURL } from "../constants";

export default {
  name: "TodoForm",
  data() {
    return {
      form: { name: "", done: false },
    };
  },
  props: {
    edit: Boolean,
    id: String,
  },
  methods: {
    async submit() {
      const { name, done } = this.form;
      if (!name) {
        return alert("Name is required");
      }
      if (this.edit) {
        await axios.put(`${APIURL}/todos/${this.id}`, { name, done });
      } else {
        await axios.post(`${APIURL}/todos`, { name, done });
      }
      this.$router.push("/todos");
    },
    async getTodo() {
      const { data } = await axios.get(`${APIURL}/todos/${this.id}`);
      this.form = data;
    },
  },
  beforeMount() {
    if (this.edit) {
      this.getTodo();
    }
  },
};
</script>
```

We have a form to let us add or edit to todo entries.

It takes the `edit` prop to indicate whether we’re editing a todo or not.

We use `v-model` to bind the inputs to the reactive properties.

Then we check for the `this.edit` value.

If it’s `true` , we make a PUT request with the `id` of the todo entry.

Otherwise, we make a POST request to create a new todo entry.

The `getTodo` method lets us get the todo by the ID when we’re editing.

Next, in the `src` folder, we create the `views` folder to add the route components.

We create the `AddTodoForm.vue` file and add:

```
<template>
  <div>
    <TodoForm></TodoForm>
  </div>
</template>

<script>
import TodoForm from "@/components/TodoForm";

export default {
  components: {
    TodoForm,
  },
};
</script>
```

We register the `TodoForm` component and render it in the template.

Next, we create the `EditTodoForm.vue` in the `views` folder and add:

```
<template>
  <div>
    <TodoForm edit :id='$route.params.id'></TodoForm>
  </div>
</template>

<script>
import TodoForm from "@/components/TodoForm";

export default {
  components: {
    TodoForm,
  },
};
</script>
```

We pass the `edit` and `id` props into the `TodoForm` so it can get todo entry by ID.

`$route.params.id` has the Object ID of the todo entry.

Next, we create the `Login.vue` file in the `views` folder to add a login form.

In this file, we add:

```
<template>
  <div>
    <h1>Login</h1>
    <form @submit.prevent="login">
      <div class="form-field">
        <label>Username</label>
        <br />
        <input v-model="form.name" type="text" />
      </div>
      <div class="form-field">
        <label>Password</label>
        <br />
        <input v-model="form.password" type="password" />
      </div>
      <div>
        <input type="submit" value="Log in" />
        <button type="button" @click="$router.push('/register')">
          Register
        </button>
      </div>
    </form>
  </div>
</template>

<script>
import axios from "axios";
import { APIURL } from "../constants";

export default {
  data() {
    return {
      form: { name: "", password: "" },
    };
  },
  methods: {
    async login() {
      const { name, password } = this.form;
      if (!name || !password) {
        alert("Username and password are required");
      }
      try {
        const {
          data: { token },
        } = await axios.post(`${APIURL}/users/login`, {
          name,
          password,
        });
        localStorage.setItem("token", token);
        this.$router.push("/todos");
      } catch (error) {
        alert("Invalid username or password.");
      }
    },
  },
};
</script>
```

We have a form that takes the username and password and let the user log in.

When we submit the form, the `login` method is called.

In the method, we check if the `name` and `password` have values.

If they both are nonempty, we proceed with making the `login` request.

If it succeeds, we go to the `/todos` route.

Otherwise, we should an error message.

Similarly, we create `Register.vue` component for the registration form.

Then we fill it with the following code:

```
<template>
  <div>
    <h1>Register</h1>
    <form @submit.prevent="register">
      <div class="form-field">
        <label>Username</label>
        <br />
        <input v-model="form.name" type="text" />
      </div>
      <div class="form-field">
        <label>Password</label>
        <br />
        <input v-model="form.password" type="password" />
      </div>
      <div>
        <input type="submit" value="Register" />
        <button type="button" @click="$router.push('/')">Login</button>
      </div>
    </form>
  </div>
</template>

<script>
import axios from "axios";
import { APIURL } from "../constants";

export default {
  data() {
    return {
      form: { name: "", password: "" },
    };
  },
  methods: {
    async register() {
      const { name, password } = this.form;
      if (!name || !password) {
        alert("Username and password are required");
      }
      try {
        await axios.post(`${APIURL}/users/register`, {
          name,
          password,
        });
        alert("Registration successful");
      } catch (error) {
        alert("Registration failed.");
      }
    },
  },
};
</script>
```

We get the username and password the same way.

If they’re both non-empty, then we make a request to the `register` route.

Once that succeeds, we show a success message.

Otherwise, we show a failure message, which is in the `catch` block.

Next, we have a component to show the todo items.

We create `Todo.vue` in the `views` folder and write:

```
<template>
  <div>
    <h1>Todos</h1>
    <button @click="$router.push(`/add-todo`)">Add Todo</button>
    <button @click="logOut">Logout</button>
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Done</th>
          <th>Edit</th>
          <th>Delete</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="t of todos" :key="t._id">
          <td>{{ t.name }}</td>
          <td>{{ t.done }}</td>
          <td>
            <button @click="$router.push(`/edit-todo/${t._id}`)">Edit</button>
          </td>
          <td>
            <button @click="deleteTodo(t._id)">Delete</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
import axios from "axios";
import { APIURL } from "../constants";

export default {
  data() {
    return {
      todos: [],
    };
  },
  methods: {
    async getTodos() {
      const { data: todos } = await axios.get(`${APIURL}/todos`);
      this.todos = todos;
    },
    async deleteTodo(id) {
      await axios.delete(`${APIURL}/todos/${id}`);
      this.getTodos();
    },
    logOut() {
      localStorage.clear();
      this.$router.push("/");
    },
  },
  beforeMount() {
    this.getTodos();
  },
};
</script>

<style scoped>
th:first-child,
td:first-child {
  width: 60%;
}

th {
  text-align: left;
}
</style>
```

We have the Add Todo button that goes to the `add-todo` route when we click it.

This will be mapped to the `AddTodoForm.vue` component.

The Logout button calls the `logOut` method when it’s clicked.

It just clears local storage and redirect the user to the login page.

The `getTodos` method gets the todo entries for the user.

The user identity is determined from the decoded token.

The `v-for` directive renders the items and display them.

The Edit button goes to the edit form.

And the Delete button calls `deleteTodo` when we click it.

`deleteTodo` makes a DELETE request to the `todos` route.

We call `getTodos` after deleting a todo entry or when we load the page.

Next in `src/App.vue` , we add our `router-view` to show the route:

```
<template>
  <router-view></router-view>
</template>

<script>
export default {
  name: "App",
};
</script>

<style>
.form-field input {
  width: 100%;
}

#app {
  margin: 0 auto;
  width: 70vw;
}
</style>
```

Then we create `constants.js` in the `src` folder and add:

```
export const APIURL = 'http://localhost:3000'
```

to let us use the `APIURL` everywhere so that we don’t have to repeat the base URL for the back end API when we make requests.

Finally, in `main.js` , we write:

```
import { createApp } from 'vue'
import App from './App.vue'
import { createRouter, createWebHashHistory } from 'vue-router';
import Login from '@/views/Login';
import Todo from '@/views/Todo';
import Register from '@/views/Register';
import AddTodoForm from '@/views/AddTodoForm';
import EditTodoForm from '@/views/EditTodoForm';
import axios from 'axios';

axios.interceptors.request.use((config) => {
  if (config.url.includes('login') || config.url.includes('register')) {
    return config;
  }
  return {
    ...config, headers: {
      Authorization: localStorage.getItem("token"),
    }
  }
}, (error) => {
  return Promise.reject(error);
});

const beforeEnter = (to, from, next) => {
  const token = localStorage.getItem('token');
  if (token) {
    next()
    return true;
  }
  next({ path: '/' });
  return false
}

const routes = [
  { path: '/', component: Login },
  { path: '/register', component: Register },
  { path: '/todos', component: Todo, beforeEnter },
  { path: '/add-todo', component: AddTodoForm, beforeEnter },
  { path: '/edit-todo/:id', component: EditTodoForm, beforeEnter },
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

const app = createApp(App);
app.use(router);
app.mount('#app')
```

to add the `vue-router` and the Axios request interceptor to add the auth token for authenticated routes.

`beforeEnter` is a route guard that lets us restrict access for authenticated routes.

We check if the token is present before we redirect the user to the page they’re going to.

Otherwise, we redirect to the login page.

`routes` has all the routes. `path` has the route URLs an `component` has the component we want to load.

`beforeEnter` is the route guard that loads before the route loads.

`createRouter` creates the router object.

`createWebHashHistory` lets us use hash mode for URLs.

So URLs will have the `#` sign between the base URL and the rest of the URL segments.

Then we call `app.use(router)` to add the router and make the `this.$route` and `thuis.$router` properties available in components.

Now we can run `npm run serve` from the `frontend` folder and `node bin/www` to start the back end.

### Conclusion

We can create a simple MEVN stack app with the latest JavaScript frameworks and features without much trouble.