Categories
Express

Error Handling with Express

Like with any other apps, we have to make Express apps ready to handle errors like unexpected inputs or file errors.

In this article, we’ll look at how to handle errors with Express.

Catching Errors

Error handling is the process of processing any errors that comes up both synchronously and asynchronously. Express comes with a default error handler so that we don’t have to write our own.

For example, if we throw errors in our route handlers as follows:

app.get('/', (req, res, next) => {
  throw new Error('error');
});

Express will catch it and proceed. We should see error instead of the app crashing.

For asynchronous errors, we have to call next to pass the error to Express as follows:

app.get('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('error');
    }
    catch (ex) {
      next(ex);
    }
  })
});

The code above will throw an error in the setTimeout callback and the catch block has the next call with the error passed in to call the built-in error handler.

We should see error instead of the app crashing.

Likewise, we have to catch rejected promises. We can do it as follows:

app.get('/', (req, res, next) => {
  Promise
    .reject('error')
    .catch(next)
});

Or with the async and await syntax, we can write the following:

app.get('/', async (req, res, next) => {
  try {
    await Promise.reject('error');
  }
  catch (ex) {
    next(ex);
  }
});

We should see error displayed instead of the app crashing with the stack trace.

The same logic also applies to routes with a chain of event handlers. We can call next as follows:

app.get('/', [
  (req, res, next) => {
    setTimeout(() => {
      try {
        throw new Error('error');
      }
      catch (ex) {
        next(ex);
      }
    })
  },
  (req, res) => {
    res.send('foo');
  }
]);

We should see the error displayed with the stack trace.

Default Error Handler

The default error handler catches the error when we call next and don’t handle it with a custom error handler.

The stack trace isn’t displayed in production environments.

If we want to send a different response than the default, we have to write our own error handler.

The only difference between route handlers, middleware and error handlers is that error handler has the err parameter before the request parameter that contains error data.

We can write a simple route with a custom event handler as follows:

app.get('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('error');
    }
    catch (ex) {
      next(ex);
    }
  })
});

app.use((err, req, res, next) => {
  res.status(500).send('Error!')
})

Note that we have the error handler below the route. The order is important. It has to below all the routes that we want to handle with it so that the error handler will get called.

We can write more than one custom error handler as follows:

app.get('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('error');
    }
    catch (ex) {
      next(ex);
    }
  })
});

app.use((err, req, res, next) => {
  if (req.foo) {
    res.status(500).send('Fail!');
  }
  else {
    next(err);
  }
})

app.use((err, req, res, next) => {
  res.status(500).send('Error!')
})

What we have above is that if req.xhr is truthy in the first error handler, then it’ll send the Fail! response and not proceed to the second one. Otherwise, the second one will be called by calling next .

So if we add req.foo = true before the setTimeout in our route handler to have:

app.get('/', (req, res, next) => {
  req.foo = true;
  setTimeout(() => {
    try {
      throw new Error('error');
    }
    catch (ex) {
      next(ex);
    }
  })
});

Then we get Fail! . Otherwise, we get Error! .

Calling next will skip to the error handler even if there’re other route handlers in the chain.

Conclusion

To handle errors, we should call next to delegate the error handling to the default event handler if no custom event handler is defined.

We can also define our own error handler function by creating a function that has the err parameter before, req , res , and next . The err parameter has the error object passed from next .

Error handlers have to be placed after all the regular route handling code so that they’ll get run.

Also, we can have multiple error handlers. If we call next on it, then it’ll proceed to the next error handler.

Categories
Express MongoDB Nodejs Vue 3

Create a Full Stack Web App with the MEVN Stack

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.

Categories
Express

Document Our Express API with the Swagger UI Express Library

Documenting APIs is painful and tedious.

However, we can make our lives easier with the Swagger UI Express library if we’re using Express.

In this article, we’ll look at how to use the library to document our Express API.

Installation

We can install the package by running:

npm i swagger-ui-express

Documenting Our Endpoints

We can document our endpoints by writing some code.

First, we add our swagger.json file by writing:

{
  "swagger": "2.0",
  "info": {
    "title": "some-app",
    "version": "Unknown"
  },
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "paths": {
    "/{id}": {
      "parameters": [
        {
          "name": "id",
          "required": true,
          "in": "path",
          "type": "string",
          "description": "some id"
        }
      ],
      "get": {
        "operationId": "routeWithId",
        "summary": "some route",
        "description": "some route",
        "produces": [
          "application/json"
        ],
        "responses": {
          "200": {
            "description": "200 response",
            "examples": {
              "application/json": "{ foo: 1 }"
            }
          }
        }
      }
    },
    "/": {
      "parameters": [],
      "get": {
        "operationId": "anotherRoute",
        "summary": "another route",
        "description": "another route",
        "produces": [
          "application/json"
        ],
        "responses": {
          "202": {
            "description": "202 response",
            "examples": {
              "application/json": "{ foo: 'bar' }"
            }
          }
        }
      }
    }
  }
}

It has all the information about the endpoints in our app.

The data includes the parameters required and the responses.

parameters has the parameters and responses has possible responses from the endpoint.

Then in our app file, we write:

index.js

const express = require('express');
const bodyParser = require('body-parser');
const swaggerUi = require('swagger-ui-express');
const swaggerDocument = require('./swagger.json');
const swaggerOptions = {
  swaggerOptions: {
    validatorUrl: null
  }
};

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, swaggerOptions));

app.get('/', (req, res) => {
  res.json({ foo: 'bar' });
});

app.get('/:id', (req, res) => {
  res.json({ foo: req.params.id });
});

app.listen(3000, () => console.log('server started'));

We require our swagger.json file and add the api-docs endpoint to show the document.

validatorUrl has the URL for Swagger’s validator for validating our document.

We just add:

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, swaggerOptions));

to add the endpoint and pass in the options for displaying the document.

The swaggerUi.setup method takes the document and options as arguments.

When we go to the /api-docs endpoint, we should see the Swagger interface and try requests with our app.

We can enter the ID for the /{id} endpoint and see the response after clicking Execute.

Custom CSS Styles

We can add custom CSS with the customCss property:

const express = require('express');
const bodyParser = require('body-parser');
const swaggerUi = require('swagger-ui-express');
const swaggerDocument = require('./swagger.json');
const swaggerOptions = {
  swaggerOptions: {
    validatorUrl: null,
  },
  customCss: '.swagger-ui .topbar { display: none }'
};

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, swaggerOptions));

app.get('/', (req, res) => {
  res.json({ foo: 'bar' });
});

app.get('/:id', (req, res) => {
  res.json({ foo: req.params.id });
});

app.listen(3000, () => console.log('server started'));

We hid the top bar with:

customCss: '.swagger-ui .topbar { display: none }'

Load Swagger from URL

We can load the Swagger file from a URL with the url option:

const express = require('express');
const app = express();
const swaggerUi = require('swagger-ui-express');

const options = {
  swaggerOptions: {
    url: 'http://petstore.swagger.io/v2/swagger.json'
  }
}

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(null, options));

We can load more than one file:

const express = require('express');
const app = express();
const swaggerUi = require('swagger-ui-express');

const options = {
  explorer: true,
  swaggerOptions: {
    urls: [
      {
        url: 'http://petstore.swagger.io/v2/swagger.json',
        name: 'Spec1'
      },
      {
        url: 'http://petstore.swagger.io/v2/swagger.json',
        name: 'Spec2'
      }
    ]
  }
}

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(null, options));

Conclusion

We can document our Express API easily with the Swagger UI Express library.

Categories
Express Nodejs Vue 3

Create a Todo App with Vue 3, Express and Sematext

Vue 3 is the up and coming version of the popular Vue front end framework.

We can pair that we the back end of our choice to create an app that we want to create.

In this article, we’ll create a Vue 3 front end that’s paired with an Express back end that uses Sematext for logging.

Get Started

We can start by creating our scaffold.

First, we create a project folder with a backend and frontend folders inside.

Then we can go into our backend folder and create our Express app.

We can use the Express generator package to make this easy.

To run it, we run:

npx express-generator

in our backend folder to add the files.

Then we run:

npm i

to install the packages.

Next, we create our Vue 3 front end project.

To do that, we go into the project folder root and run:

npm init vite-app frontend

This will create the project files in the frontend folder and install the required packages.

Backend

Now we have the scaffolding for both apps, we can work on the back end app.

We install a few more packages that we’ll need for our back end.

To do that we run:

npm i cors dotenv sematext-agent-express sqlite3

cors is a package to let us communicate between front end and back end regardless of the domain they’re in.

dotenv lets us read the environment variables.

sematext-agent-express is the Sematext package for Express apps.

sqlite3 lets us save data to a SQLite database.

Next, we create a todo.js file in the routes folder.

And then in app.js, we change the existing code to:

require('dotenv').config()
const { stHttpLoggerMiddleware } = require('sematext-agent-express')
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 indexRouter = require('./routes/index');
var todosRouter = require('./routes/todo');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(cors())

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(stHttpLoggerMiddleware)

app.use('/', indexRouter);
app.use('/todos', todosRouter);



// 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 added the todosRouter middleware to let us use the todos route file.

And we have:

app.use('/todos', todosRouter);

to use the todosRouter file.

Also, we have:

require('dotenv').config()

to read the .env file which we’ll need for reading the API token for Sematext.

We add the Sematext Express middleware into our app with:

app.use(stHttpLoggerMiddleware)

so we can use its logging capabilities in our app.

We also have:

var cors = require('cors');

to let us do cross-domain communication.

Next, we go to todo.js and replace the existing code with:

var express = require('express');
var sqlite3 = require('sqlite3').verbose();
const { stLogger } = require('sematext-agent-express')

var db = new sqlite3.Database('./todos.db');
var router = express.Router();

router.get('/', (req, res) => {
  db.serialize(() => {
    db.all("SELECT * FROM todos", (err, results) => {
      if (err) {
        stLogger.error(err);
        return res.status(500).send(err);
      }
      stLogger.info(results);
      res.json(results);
    });
  });
});

router.post('/', (req, res) => {
  const { name } = req.body;

  db.serialize(() => {
    db.run("INSERT INTO todos (name) VALUES (?)", name, (err, results) => {
      if (err) {
        stLogger.error(err);
        return res.status(500).send(err);
      }
      stLogger.info(results);
      res.json(results);
    });
  });
});

router.put('/:id', (req, res) => {
  const { id } = req.params;
  const { name } = req.body;

  db.serialize(() => {
    db.run("UPDATE todos SET name = ? WHERE id = ?", name, id, (err, result) => {
      if (err) {
        stLogger.error(err);
        return res.status(500).send(err);
      }
      stLogger.info(result);
      res.json(result);
    });
  });
});

router.delete('/:id', (req, res) => {
  const { id } = req.params;
  db.serialize(() => {
    db.run("DELETE FROM todos WHERE id = ?", id, (err, results) => {
      if (err) {
        stLogger.error(err);
        return res.status(500).send(err);
      }
      stLogger.info(results);
      res.json(results);
    });
  });
});

module.exports = router;

We add the routes with the router methods.

We use the stLogger object from the sematext-agent-express package to let us do the logging.

In each route, we have the stLogger.error method to log errors.

And we have stLogger.info to log other information like the database results.

Each time the route middleware runs, we’ll see something logged.

If there’s an error we return that as the response back to the client with the if statements.

We call db.serialize to run database queries in sequence.

And we use db.run to run INSERT, UPDATE, DELETE statements.

To run SELECT queries, we run db.all to get all the rows.

We use parameterized queries so that values are escaped before we run the queries.

req.params gets the request parameters from the URL.

req.body gets the request body.

We have:

var db = new sqlite3.Database('./todos.db');

The todos.db file will be created if it doesn’t exist.

Once it’s created we can open the file by using the SQLite browser from https://sqlitebrowser.org/.

We can download the file and install it.

Then we can open the todos.db file and run:

CREATE TABLE todos (id INTEGER PRIMARY KEY, name TEXT NOT NULL)

to create the todos table so we can write to it.

Now the SQL statements in our code should run properly.

Then we go into the backend folder and create the .env file.

And then we add the LOGS_TOKEN key so that we can use Sematext for logging:

LOGS_TOKEN=YOUR_OWN_KEY_FROM_SEMATEXT

We can get the key by signing up for an account by going to https://apps.sematext.com/ui/login/.

Once we’re in, we can click on Apps on the left menu.

Then click on New App on the top right to create our app.

Once you see the app entry, we click on the menu button on the right side of the row, and click Integrations.

Then we see Node.js on the left side of what’s open and follow the instructions from there.

Front End

Now that the back end is done, we can move onto the front end.

First, we install the axios HTTP client so that we can make HTTP requests.

We can do that by running:

npm i axios

We go to the frontend/components folder and create a Todo.vue file.

Then we add:

<template>
  <form @submit.prevent="save">
    <input type="text" v-model="todo.name" />
    <input type="submit" value="save" />
    <button type="button" @click="deleteTodo" v-if='todo.id'>delete</button>
  </form>
</template>

<script>
import axios from "axios";
const APIURL = "http://localhost:3000";

export default {
  name: "Todo",
  props: {
    todo: {
      type: Object,
      default() {
        return {};
      },
    },
  },
  data() {
    return {
      name: "",
    };
  },
  methods: {
    async save() {
      const { name } = this.todo;
      if (this.todo.id) {
        await axios.put(`${APIURL}/todos/${this.todo.id}`, { name });
      } else {
        await axios.post(`${APIURL}/todos`, { name });
      }
      this.$emit("saved-todo");
    },

    async deleteTodo() {
      const { data } = await axios.delete(`${APIURL}/todos/${this.todo.id}`);
      this.$emit("saved-todo");
    },
  },
};
</script>

to it.

We use the form to add or edit the files.

Also, we have a delete button to delete the todos.

The save method makes a PUT request to our back end if the todo has an id.

This mneans it’s an existing entry, so we make a PUT request.

Otherwise, we make a POST request.

Once they’re successful, then we emit the save-todos event so that we can get the latest data later.

Also, we have the deleteTodo method so that we can make a DELETE request to delete a item.

The Todo component takes a todo prop, which is optional.

We have the default method to return the default value for it.

The v-if in the template checks if the todo.id exists.

If it does, then it’s displayed so we can call delete to delete the todo item.

We also have a @subnmit.prevent directive to let us submit our form and run the save method.

Next we work on the App.vue file.

We write:

<template>
  <div>
    <Todo @saved-todo="getTodos"></Todo>
    <Todo @saved-todo="getTodos" v-for="t of todos" :todo="t" :key="t.id"></Todo>
  </div>
</template>

<script>
import axios from "axios";
import Todo from "./components/Todo.vue";
const APIURL = "http://localhost:3000";

export default {
  name: "App",
  components: {
    Todo,
  },
  data() {
    return {
      todos: [],
    };
  },
  beforeMount() {
    this.getTodos();
  },
  methods: {
    async getTodos() {
      const { data } = await axios.get(`${APIURL}/todos`);
      this.todos = data;
    },
  },
};
</script>

We add the Todo component that we created earlier.

The first one is for adding the todo item.

The getTodos method lets us get the todo items.

It’s run when the saved-todo event is emitted.

We listen to the event with @saved-todo on the template.

Also, we call getTodos in the beforeMount hook so that we can get the todos when the page loads.

Running Our App

Once this is done, we can run our app.

We first go into the backend folder and run:

npm start

Then we go into the frontend folder and run:

npm run dev

Once that’s done, we can go to http://localhost:3001 to see the app.

Now we should see:

when we go to http://localhost:3001 and when we do something in our app, we see something like:

logged in Sematext.

It’ll log your activities.

Conclusion

Using Sematext with an Express app is easy.

We just use the sematext-express-agent package to let us log with it.

Creating a Vue front end is also easy.

Vue 3 is the up and coming version of Vue. It’s almost ready for production.

Categories
Express Nodejs Testing

Add Tests to Express Apps With Jest and SuperTest

Automated tests are essential to the apps we write since modern apps have so many moving parts.

In this piece, we’ll look at how to write apps to test an Express app that interacts with a database with Jest and SuperTest.


Creating the App We’ll Test

We create a project folder by creating an empty folder and running the following to create a package.json file with the default answers:

npm init -y

Then we run the following to install the packages for our apps:

npm i express sqlite3 body-parser

Then, we create the app.js file for our app and write:

const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const bodyParser = require('body-parser');
const app = express();
const port = process.env.NODE_ENV === 'test' ? 3001 : 3000;
let db;
if (process.env.NODE_ENV === 'test') {
    db = new sqlite3.Database(':memory:');
}
else {
    db = new sqlite3.Database('db.sqlite');
}

db.serialize(() => {
    db.run('CREATE TABLE IF NOT EXISTS persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
});

app.use(bodyParser.json());
app.get('/', (req, res) => {
    db.serialize(() => {
        db.all('SELECT * FROM persons', [], (err, rows) => {
            res.json(rows);
        });
    })
})

app.post('/', (req, res) => {
    const { name, age } = req.body;
    db.serialize(() => {
        const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');
        stmt.run(name, age);
        stmt.finalize();
        res.json(req.body);
    })
})

app.put('/:id', (req, res) => {
    const { name, age } = req.body;
    const { id } = req.params;
    db.serialize(() => {
        const stmt = db.prepare('UPDATE persons SET name = ?, age = ? WHERE id = ?');
        stmt.run(name, age, id);
        stmt.finalize();
        res.json(req.body);
    })
})

app.delete('/:id', (req, res) => {
    const { id } = req.params;
    db.serialize(() => {
        const stmt = db.prepare('DELETE FROM persons WHERE id = ?');
        stmt.run(id);
        stmt.finalize();
        res.json(req.body);
    })
})

const server = app.listen(port);
module.exports = { app, server };

The code above has the app we’ll test.

To make our app easier to test, we have:

const port = process.env.NODE_ENV === 'test' ? 3001 : 3000;
let db;
if (process.env.NODE_ENV === 'test') {
    db = new sqlite3.Database(':memory:');
}
else {
    db = new sqlite3.Database('db.sqlite');
}

So we can set the process.env.NODE_ENV to 'test' to make our app listen to a different port than it does when the app is running in a nontest environment.

We’ll use the 'test' environment to run our tests.

Likewise, we want our app to use a different database when running unit tests than when we aren’t running them.

This is why we have:

let db;
if (process.env.NODE_ENV === 'test') {
    db = new sqlite3.Database(':memory:');
}
else {
    db = new sqlite3.Database('db.sqlite');
}

We specified that when the app is running in a 'test' environment we want to use SQLite’s in-memory database rather than a database file.


Writing the Tests

Initialization the code

With the app made to be testable, we can add tests to it.

We’ll use the Jest test runner and SuperTest to make requests to our routes in our tests. To add Jest and SuperTest, we run:

npm i jest supertest

Then, we add app.test.js to the same folder as the app.js file we had above.

In app.test.js, we start by writing the following:

const { app } = require('./app');
const sqlite3 = require('sqlite3').verbose();
const request = require('supertest');
const db = new sqlite3.Database(':memory:');

beforeAll(() => {
    process.env.NODE_ENV = 'test';
})

In the code above, we included our Express app from our app.js. Then we also included the SQLite3 and SuperTest packages.,

Then, we connected to our in-memory database with:

const db = new sqlite3.Database(':memory:');

Next, we set all the tests to run in the 'test' environment by running:

beforeAll(() => {
    process.env.NODE_ENV = 'test';
})

This will make sure we use port 3001 and the in-memory database we specified in app.js for each test.

To make our tests run independently and with consistent results, we have to clean our database and insert fresh data every time.

To do this, we create a function we call on each test:

const seedDb = db => {
    db.run('CREATE TABLE IF NOT EXISTS persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
    db.run('DELETE FROM persons');
    const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');
    stmt.run('Jane', 1);
    stmt.finalize();
}

The code above creates the persons table if it doesn’t exist and deletes everything from there afterward.

Then we insert a new value in there to have some starting data.


Adding Tests

With the initialization code complete, we can write the tests.

GET request test

First, we write a test to get the existing seed data from the database with a GET request.

We do this by writing:

test('get persons', () => {
    db.serialize(async () => {
        seedDb(db);
        const res = await request(app).get('/');
        const response = [
            { name: 'Jane', id: 1, age: 1 }
        ]
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

We put everything inside the callback of db.serialize so the queries will be run sequentially.

First, we call seedDb, which we created above to create the table if it doesn’t exist, to clear out the database, and to add new data.

Then, we call the GET request by writing:

await request(app).get('/');

This gets us the res object with the response resolved from the promise.

request(app) will start the Express app so we can make the request.

Next, we have the response for us to check against for correctness:

const response = [
  { name: 'Jane', id: 1, age: 1 }
]

Then, we check the responses to see if we get what we expect:

expect(res.status).toBe(200);
expect(res.body).toEqual(response);

The toBe method checks for shallow equality, and toEqual checks for deep equality. So we use toEqual to check if the whole object structure is the same.

res.status checks the status code returned from the server, and res.body has the response body.

POST request test

Next, we add a test for the POST request. It’s similar to the GET request test.

We write the following code:

test('add person', () => {
    db.serialize(async () => {
        seedDb(db);
        await request(app)
            .post('/')
            .send({ name: 'Joe', age: 2 });

        const res = await request(app).get('/');
        const response = [
            { name: 'Jane', id: 1, age: 1 },
            { name: 'Joe', id: 2, age: 2 }
        ]
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

First, we reset the database with:

seedDb(db);

We made our POST request with:

await request(app)
  .post('/')
  .send({ name: 'Joe', age: 2 });

This will insert a new entry into the in-memory database.

Finally, to check for correctness, we make the GET request — like in our first test — and check if both entries are returned:

const res = await request(app).get('/');
const response = [
  { name: 'Jane', id: 1, age: 1 },
  { name: 'Joe', id: 2, age: 2 }
]
expect(res.status).toBe(200);
expect(res.body).toEqual(response);

PUT and DELETE tests

The test for the PUT request is similar to the POST request. We reset the database, make the PUT request with our payload, and then make the GET request to get the returned data, as follows:

test('update person', () => {
    db.serialize(async () => {
        seedDb(db);
        await request(app)
            .put('/1')
            .send({ name: 'Joe', age: 2 });

        const res = await request(app).get('/');
        const response = [
            { name: 'Jane', id: 1, age: 1 }
        ]
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

Then we can replace the PUT request with the DELETE request and test the DELETE request:

test('delete person', () => {
    db.serialize(async () => {
        seedDb(db);
        const res = await request(app).delete('/1');
        const response = [];
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

Running the Tests

To run the tests, we add the following to the scripts section:

"test": "jest --forceExit"

We have to add the --forceExit option so Jest will exist after the tests are run. There’s no fix for the issue where Jest tests using SuperTest don’t exit properly yet.

Then we run the following to run the tests:

npm test

And we should get:

PASS  ./app.test.js
  √ get persons (11ms)
  √ add person (2ms)
  √ update person (2ms)
  √ delete person (6ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        2.559s
Ran all test suites.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

We should get the same thing no matter how many times we run the tests since we reset the database and made all database queries run sequentially.

Also, we used a different database and port for our tests than other environments, so the data should be clean.


Conclusion

We can add tests run with the Jest test runner. To do this, we have to have a different port and database for running the tests. Then we create the tables if they don’t already exist, clear all the data, and add seed data so we have the same database structure and content for every test.

With SuperTest, we can run the Express app automatically and make the request we want. Then, we can check the output.