Alexander Nnakwue Software engineer. React, Node.js, Python, and other developer tools and libraries.

How to implement JWT authentication with Vue and Node.js

16 min read 4725

Authentication Vue Node

Editor’s note: This article was last updated on 23 March 2022 to correct bugs in the sample code and include up-to-date packages. 

JWT, an acronym for JSON Web Token, is an open standard that allows developers to verify the authenticity of a type of information known as a claim via a signature. The signature can either be a secret or a public/private key pair. Together with the header and the payload, a signature can be used to generate or construct a JWT.

JWTs are commonly used for either authentication or to safely transmit information across different parties. Once a user has logged into an app, a JWT is created on the server and returned back to the calling client, which is a common flow for JWT-based authentication systems.

Each subsequent request will include the JWT as an authorization header, allowing access to protected routes and resources. Once the backend server verifies that the signature is valid, it extracts the user data from the token as required. To ensure that a JWT is valid, only the party holding the key or secret is responsible for signing the information.

In this article, we’ll learn how to perform authentication requests on a Vue client app with a Node.js backend. But first, let’s review how JWT works in detail.

Table of contents

How does JWT authentication work?

In JWT authentication-based systems, when a user successfully logs in using their credentials, a JSON Web Token will be returned back to the calling client. Whenever the user wants to access a protected route or resource, the user agent sends the same JWT, typically in the Authorization header using the Bearer schema.

The content of the header should look like the syntax below:

Authorization: Bearer <token>

The server routes will have to check for the presence of a valid JWT in the Authorization header to grant a user access to a protected resource. As a bonus, sending JWTs in the Authorization header also solves some issues related to CORS, which applies even if the app is served from an entirely different domain.

Even if JWTs are signed, the information is still exposed to users or other parties because the data is unencrypted. Therefore, users are encouraged to not include sensitive information like credentials within a JWT payload. Additionally, tokens should always have an expiry.

Bootstrapping our Node.js application

To set up our Vue application with JWT as a means of authenticating to a backend Node.js server, first, we’ll build out the backend part of the application, which handles both generating and subsequently verifying the JWT. The full code for this tutorial can be found in this GitHub repo.

Install dependencies

First, let’s open our terminal and install all the necessary dependencies for our app, including the jsonwebtoken package from npm, Express for our server, and babel-polyfill, which provides the required polyfills for a full ES2015+ environment.

For a complete list of the dependencies required for our application, check out the package.json file; the entire contents are shown below:

  "name": "jwt-vue-node-backend",
  "version": "1.0.0",
  "description": "Backend server for the JWT-Vue-Node-App",
  "main": "server.js",
  "scripts": {
    "create-dev-tables": "babel-node ./app/db/dbConnection createUserTable",
    "drop-dev-tables": "babel-node ./app/db/dbConnection dropUserTable",
    "dev": "nodemon --watch . --exec babel-node -- server/server",
    "start-dev": "babel-node server/server.js",
    "build-server": "babel server -d dist",
    "build": "rm -rf dist && mkdir dist && npm run build-server",
    "start-prod": "node dist/server.js",
    "start": "npm-run-all -p start-prod",
    "revamp": "npm-run-all -p dev drop-dev-tables"
  "esModuleInterop": true,
  "keywords": [
  "author": "Alexander Nnakwue",
  "license": "MIT",
  "devDependencies": {
    "@babel/cli": "^7.12.1",
    "@babel/core": "^7.12.3",
    "@babel/node": "^7.12.10",
    "@babel/preset-env": "^7.12.1",
    "@babel/register": "^7.12.10",
    "babel-preset-es2015": "^6.24.1",
    "babel-watch": "^7.0.0",
    "eslint": "^7.11.0",
    "eslint-config-airbnb-base": "^14.2.0",
    "eslint-plugin-import": "^2.22.1",
    "nodemon": "^2.0.5"
  "dependencies": {
    "@hapi/joi": "^17.1.1",
    "babel-plugin-add-module-exports": "^1.0.4",
    "babel-polyfill": "^6.26.0",
    "bcryptjs": "^2.4.3",
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "make-runnable": "^1.3.8",
    "moment": "^2.29.1",
    "npm-run-all": "^4.1.5",
    "pg": "^8.4.1"
  "engines": {
    "node": "13.7.0",
    "npm": "6.13.6"

As seen in the file above, we’ve installed the necessary packages to make Babel work as a development dependency. We began by making sure we installed the Babel CLI globally, then we checked for other dependencies like eslint, nodemon, pg, etc.

Pay special attention to the scripts section of the package.json file, which shows us how to create and drop the user table, build the server file of our app, and run the app on our development environment. The scripts section also shows us how to run our app in production.

Our final folder structure should look like the following:

Folder Structure JWT Node Vue App

Folder structure for our completed app

As you can see, our app is a monorepo, which will easily allow us to add the client folder, build, and output the build to a dist directory. Lastly, we’ll add the build path to our server.js file.

The app folder contains all the extra components needed for our backend to run successfully, including the controllers, the database handlers, helpers, the middleware folders, and more. After cloning the repo, be sure to remove the following two lines from ./app/db/dbconnection.js:

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

As we mentioned earlier, the client folder contains everything that has to do with the user-facing part of the app. The server folder contains the server.js file, a basic Express server that powers our application:

import express from 'express'
import 'babel-polyfill'
import cors from 'cors'
import usersRoute from '../app/route/user.js'
import path from 'path'

const __dirname = path.resolve();
const app = express()

// Add  cors middleware

// Add middleware for parsing JSON and urlencoded data
app.use(express.urlencoded({ extended: false }))

// Serve static files from the Vue app

// The "catchall" handler: for any request that doesn't
// match one above, send back Vue's index.html file.
app.get('/*', (req, res) => {

app.use('/api/v1', usersRoute)

app.get('/', (req, res) => {
    res.status(200).send('Welcome to the JWT authentication with Node and Vue.js tutorial')

// catch 404 and forward to error handler
app.use((req, res, next) => {
    const err = new Error('Not Found')
    err.status = 404
    res.send('Route not found')

const server = app.listen(process.env.PORT || 3000).on('listening', () => {
    console.log(`App live and listening on port: ${process.env.PORT}` || 3000)

export default app

Before we review everything happening in the server file above, we need to set up the Babel config for our application. The .babelrc file is shown below:

    "presets": ["@babel/preset-env"],
    "plugins": ["add-module-exports"]

The preset we’re using above lets us write the latest JavaScript syntax, eliminating the need to check which syntax transforms and, optionally, which browser polyfills are needed by the OS target environment. The plugin allows Babel to understand and interpret our module.exports statements and transforms.

In the server.js file above, we’re serving static files and assets from our client build path. We also import our routes from the route path. Lastly, we import the babel-polyfill package, which we explained above. Check out the repo for the eslint and other setups.

Now, we’ll quickly set up our config file, which picks variables from our .env file:

const { env } = process

module.exports = {
    name: env.APP_NAME,
    baseUrl: env.APP_BASE_URL,
    port: env.APP_PORT,
    databases: {
        postgres: {
            user: env.PG_USERNAME,
            password: env.PG_PASSWORD,
            host: env.PG_HOST,
            port: env.PG_PORT,
            db_name: env.PG_DATABASE,
            url: env.PG_URL,

Postgres is our choice database for this tutorial, meaning we need to provide the necessary details to connect to a live Postgres DB. Now, let’s look at the most important part of the codebase, which is under the app folder. Basically, all the files located here are required to make our server respond to HTTP requests.

Let’s start with the user.js file in the routes folder:

const express = require('express')
const { registerUser, loginUser, me } = require('../controller/users.js')
const  {
} = require( '../helpers/validation.js')
const {
 } = require( '../middleware/verifyAuth.js')

const router = express.Router()

// user Routes'/auth/signup', createUser, registerUser)'/auth/login', loginUser)
router.get('/me', decodeHeader, me)

module.exports = router

We need just three endpoints to demonstrate how JWT works with Vue and Node.js. We import the controller file and the middleware file .../middleware/verifyAuth.js, which is used to decode the Authorization header sent from the client. We’ll explore that in a moment.

For now, let’s look at the controller found in the user.js file via the ~/controller/users.js path. Let’s explore the login.js method as an example:

   * login User
   * @param {object} req
   * @param {object} res
   * @returns {object} user object

const loginUser = async (req, res) => {
    const { email, password } = req.body

    if (isEmpty(email) || isEmpty(password)) {
        return Response.sendErrorResponse({
            message: 'Email or Password detail is missing',
            statusCode: 400,

    if (!isValidEmail(email) || !validatePassword(password)) {
        return Response.sendErrorResponse({
            message: 'Please enter a valid Email or Password',
            statusCode: 400,

    const loginUserQuery = 'SELECT * FROM users WHERE email = $1'

    try {

        const { rows } = await dbQuery.query(loginUserQuery, [email])
        const dbResponse = rows[0]

        if (!dbResponse) {
            return Response.sendErrorResponse({
                message: 'User with this email does not exist',
                statusCode: 400,

        if (!comparePassword(dbResponse.password, password)) {
            return Response.sendErrorResponse({
                message: 'The password you provided is incorrect',
                statusCode: 400,

        const token = Utils.generateJWT(dbResponse)
        const refreshExpiry = moment().utc().add(3, 'days').endOf('day')
        const refreshtoken = Utils.generateJWT({ exp: parseInt(refreshExpiry), data: dbResponse._id })
        delete dbResponse.password
        return Response.sendResponse({
            responseBody: { user: dbResponse, token, refresh: refreshtoken },
            message: 'login successful',
    } catch (error) {
        return Response.sendErrorResponse({
            message: error,
            statusCode: 500,

After we finish with the usual password comparison, we generate a JWT with the details of the user returned from querying our pg database. Next, we generate a refresh token with refreshExpiry. Finally, we return the token and the refresh token as part of the response:

const token = Utils.generateJWT(dbResponse)
const refreshExpiry = moment().utc().add(3, 'days').endOf('day')
const refreshtoken = Utils.generateJWT({ exp: parseInt(refreshExpiry), data: dbResponse._id })

I’ve already set up a Postgres database with the Heroku Postgres add-on. We’ll discuss the details later when we deploy our app to the Heroku environment.

Now, let’s look at the Utils folder where the generateJWT method is defined. We import the Utils module at the top of the controller file on line 10. Navigating to the helpers/utils.js path, we’ll learn firsthand how to verify and sign tokens:

const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const fs = require('fs')

const privateKEY = fs.readFileSync('./private.key', 'utf8')
const publicKEY = fs.readFileSync('./public.key', 'utf8')

const i = 'jwt-node'
const s = 'jwt-node'
const a = 'jwt-node'

const verifyOptions = {
    issuer: i,
    subject: s,
    audience: a,
    expiresIn: '8784h',
    algorithm: ['RS256'],

const saltRounds = 10

const salt = bcrypt.genSaltSync(saltRounds)

    const generateJWT = (payload) => {
        const signOptions = {
            issuer: i,
            subject: s,
            audience: a,
            expiresIn: '8784h',
            algorithm: 'RS256',

        const options = signOptions
        if (payload && payload.exp) {
            delete options.expiresIn
        return jwt.sign(payload, privateKEY, options)

    const verifyJWT = (payload) => {
        return jwt.verify(payload, publicKEY, verifyOptions)

    const hashPassword = (password) => {
        const hash = bcrypt.hashSync(password, salt)
        return hash

module.exports = {
    hashPassword, verifyJWT, generateJWT

The generateJWT method synchronously signs the given payload into a JSON Web Token string by using a privateKEY and an options argument.

As we can see from the code snippet above, there are two major methods we’re interested in, verifyJWT and generateJWT. Let’s explore the latter method, which follows the JWT creation approach and is constructed from three different elements, the header, the payload, and the signature/encryption data, separated by dots ..

JWT structure

As we just mentioned, JWTs are constructed from three different elements, including the header, the payload, and the signature.

The header is a JSON object that contains claims about itself, including the type of token. It also contains the signing algorithm being used, like HMAC, SHA-256 with RSA, or ES256. We’re using the RS256 algorithm. It also defines whether the JWT is signed or encrypted:

const signOptions = {
            issuer: i,
            subject: s,
            audience: a,
            expiresIn: '8784h',
            algorithm: 'RS256',
            "typ": "JWT"

The second part of the token is the payload, which usually contains details about the user. It may also contain some claims that are statements about that user. There are three types of claims, include registered, public, and private claims. Some of these claims are iss, issuer, exp, expiration time, sub, subject, and aud, audience.

The structure of a sample payload is shown below:

  id: 2,
  email: '[email protected]',
  firstname: 'Kelechi',
  lastname: 'Oti',
  password: '$2a$10$yK1bInjD2KieruxwFE9UV.ZuYiwZHu8ciX2vMZmLSvbrOdZ4/Maju',
  createdon: 2020-10-31T23:00:00.000Z

Note: For signed tokens, this information, though protected against tampering, can be read by anyone. Ensure that you don’t put secret information in the payload or header elements of a JWT unless it is encrypted.

The third part is the signature, which is used to verify that the message wasn’t changed along the way. In the case of tokens signed with a private key, it can also verify that the sender of the JWT is who they say they are. The signature is largely dependent on the algorithm used for signing or encryption. In the case of unencrypted JWTs, there is none. In essence, for unencrypted JWTs, the header is shown below:

    "alg": "none"

Our JWT is encrypted and signed by a private/public key pair available in the root path of our repo. Let’s look at a sample JWT below. Notice the dots separating the three elements of the JWT, the header, the payload, and the signature, respectively, which are base64url-encoded:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJhbGV4Lm5uYWt3dWVAZ21haWwuY29tIiwiZmlyc3RuYW1lIjoiS2VsZWNoaSIsImxhc3RuYW1lIjoiT3RpIiwicGFzc3dvcmQiOiIkMmEkMTAkeUsxYkluakQyS2llcnV4d0ZFOVVWLlp1WWl3Wkh1OGNpWDJ2TVptTFN2YnJPZFo0L01hanUiLCJjcmVhdGVkb24iOiIyMDIwLTExLTAxVDAwOjAwOjAwLjAwMFoiLCJpYXQiOjE2MDgyNjY5MDYsImV4cCI6MTYzOTg4OTMwNiwiYXVkIjoicXVlc3Rpb25zLWdhbWUiLCJpc3MiOiJxdWVzdGlvbnMtZ2FtZSIsInN1YiI6InF1ZXN0aW9ucy1nYW1lIn0.FLXTabVVpZ_zuPEspoQe8U5kr8CwKNFKgXBwgQN6yKkW2RXmQGXyuML3dLy4XOEVEgaNoAjSI8De65CKlAq_dA is an interactive playground for learning more about JWTs. For example, we can go ahead and copy the token from above and see what happens when we edit it.

Next, let’s look at the verifyJWT method, which basically allows us to use the publicKEY to verify that the privateKEY was responsible for signing the header and the payload. A snippet of the verifyJWT method is shown below:

const verifyJWT = (payload) => {
        return jwt.verify(payload, publicKEY, verifyOptions)

JWT has a verify method that synchronously verifies a given token using a secret or a public key and options for the verification. Once the token is verified, a decoded value of that token is returned.

More great articles from LogRocket:

Now, when we log into the app, we can see the user object, the token, and the refresh token, as shown below:

    "data": {
        "user": {
            "id": 2,
            "email": "[email protected]",
            "firstname": "Kelechi",
            "lastname": "Oti",
            "createdon": "2020-10-31T23:00:00.000Z"
        "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJhbGV4Lm5uYWt3dWVAZ21haWwuY29tIiwiZmlyc3RuYW1lIjoiS2VsZWNoaSIsImxhc3RuYW1lIjoiT3RpIiwicGFzc3dvcmQiOiIkMmEkMTAkeUsxYkluakQyS2llcnV4d0ZFOVVWLlp1WWl3Wkh1OGNpWDJ2TVptTFN2YnJPZFo0L01hanUiLCJjcmVhdGVkb24iOiIyMDIwLTEwLTMxVDIzOjAwOjAwLjAwMFoiLCJpYXQiOjE2MDgyNzMxNTgsImV4cCI6MTYzOTg5NTU1OCwiYXVkIjoiand0LW5vZGUiLCJpc3MiOiJqd3Qtbm9kZSIsInN1YiI6Imp3dC1ub2RlIn0.ky4EYUP3t10qaH1aCc0g_jxX5zt8U03l7lrwfpVjViYGqufxRcLoYNV8rw8SygxouXOqSPiF_LxLGv3fi4MQyQ",
        "refresh": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDg1OTUxOTksImlhdCI6MTYwODI3MzE1OCwiYXVkIjoiand0LW5vZGUiLCJpc3MiOiJqd3Qtbm9kZSIsInN1YiI6Imp3dC1ub2RlIn0.VvWQ17Wzyw-tJdRiIxsVzXNhl-nQSPf6Wo1iFOtC3BeueywB1lLaqOK4rmO0UAxKovx6sjZufu2mXEubKsiLOg"
    "status": true,
    "message": "login successful"

Token and refresh tokens

Tokens give users access to protected resources. They are usually short-lived and may have an expiration date attached to their headers. They may also contain additional information about the user.

Refresh tokens, on the other hand, allow users to request new tokens. For example, after a token has expired, a client may perform a request for a new token to be generated by the backend server, which requires a refresh token. In contrast to access tokens, refresh tokens are usually long-lived.

The verifyAuth file, located in the middleware folder, contains middleware methods responsible for decoding the Authorization sent from the client and validating the token. We’re interested in the decodeHeader method in verifyAuth, shown below:

const decodeHeader = (req, res, next) => {
    let token = req.headers['x-access-token'] || req.headers.authorization || req.body.token
    if (!token) {
        return Response.sendErrorResponse({ res, message: 'No token provided', statusCode: 401 })
    if (token.startsWith('Bearer ')) {
        // Remove Bearer from string
        token = token.slice(7, token.length)
        if (!token || token === '') Response.sendErrorResponse({ res, message: 'No token provided', statusCode: 401 })
    // call the verifyJWT method to verify the token is valid
    const decoded = Utils.verifyJWT(token)
    if (!decoded) Response.sendErrorResponse({ res, message: 'invalid signature', statusCode: 403 })
    // attach the decoded token to the res.user object
    if (decoded) res.user = decoded
    res.token = token
    return next()

As we can see from the decodeHeader method above, we accept the token from the client in the form of authorization headers or in the req.body. If no token is provided, we return an error. We also check whether the token comes with the Bearer schema; if it does, we call the verifyJWT method from the Utils module.

This method, as we previously discussed, verifies the token sent from the client using the public key and returns the decoded value of the token. Note that if the decoded value is not returned, it means the token initially signed is invalid. Lastly, we append the decoded value to the response object.

Since we’re majorly focused on JWT authentication, we won’t cover some parts of the app. For example, for details about the database connections, you can check the db folder, located in the app directory.

Building our client-side Vue app

Now, let’s focus on the client-side part of the application. To bootstrap a Vue app, we can navigate to our terminal or command prompt and run the following command in the root folder:

vue create <folder-name>

If you haven’t already, go ahead and install the Vue CLI globally on your machine with either of the commands below:

npm install -g @vue/cli
yarn global add @vue/cli

To get a feel for the dependencies we might need for the client app, take look at the package.json file:

  "name": "vue-jwt-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "heroku-postinstall": "npm install && npm run build"
  "proxy": "http://localhost:3000",
  "dependencies": {
    "@babel/preset-env": "7.3.4",
    "axios": "^0.20.0",
    "bootstrap": "^4.5.3",
    "bootstrap-vue": "^2.18.0",
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "vue-jwt-decode": "^0.1.0",
    "vue-router": "^3.4.7"
  "devDependencies": {
    "@vue/babel-preset-app": "^4.5.8",
    "@vue/cli-plugin-babel": "^4.5.8",
    "@vue/cli-plugin-eslint": "^4.5.0",
    "@vue/cli-service": "^4.5.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-config-prettier": "^6.14.0",
    "eslint-plugin-prettier": "^3.1.4",
    "eslint-plugin-vue": "^6.2.2",
    "less": "^3.12.2",
    "less-loader": "^7.0.2",
    "vue-template-compiler": "^2.6.11"
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    "extends": [
    "parserOptions": {
      "parser": "babel-eslint"
    "rules": {}
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  "Author": "Alexander Nnakwue"

As we can see from the package.json file above, we’ve installed a couple of dependencies needed for our client app to run. Most importantly, we installed vue-jwt-decode, a JWT decoder for Vue.js applications.

We can see that we are using Axios to make API calls to our backend server. Other important dependencies needed for our client app to run can be found in the package.json file above.

Navigate to the src/components/auth folder. There, you’ll find all the necessary components for our client app. Let’s look at the login component first. When a user logs into our app, we save the generated JWT to local storage. Lets see what the login component looks like:

  <div class="container">
      <b-card-text>Already have an account? Login Here!</b-card-text>
      <div class="row">
        <div class="col-lg-6 offset-lg-3 col-sm-10 offset-sm-1">
            class="text-center border border-primary p-5"
            style="margin-top:70px;height:auto;padding-top:100px !important;"
              class="form-control mb-5"
            <!-- Password -->
              class="form-control mb-5"
              Dont have an account? Click
              <router-link to="/register"> here </router-link> to sign up
            <!-- Sign in button -->
              <button class="btn btn-primary btn-block w-75 my-4" type="submit">
                Sign in
export default {
  data() {
    return {
      login: {
        email: "",
        password: ""
  methods: {
    async loginUser() {
      try {
        let response = await this.$"/auth/login", this.login);
        let token =;
        localStorage.setItem("user", token);
        // navigate to a protected resource 
      } catch (err) {

We use BootstrapVue to style our components. Once we log into our app via the backend API, we set the returned token to localStorage. Once that happens, we navigate to the Home component using the router push API.

To demonstrate the entire flow, we retrieve the token from local storage, decode the token using the vue-jwt-decode package, then finally append that to the user object, which we can then display in our Home component:

    <b-navbar id="navbar" toggleable="md" type="dark" variant="info">
      <b-navbar-brand href="#">
        You are currently viewing the Vue_JWT_App
      <b-navbar-nav class="ml-auto">
        <b-nav-text>{{ user.firstname }} | </b-nav-text>
        <b-nav-item @click="logUserOut" active>Logout</b-nav-item>
      <div class="container mt-5">
        <div class="row">
          <div class="col-md-12">
            <ul class="list-group">
              <li class="list-group-item">
                Name : {{ user.firstname }} {{ user.lastname }}
              <li class="list-group-item">Email : {{ }}</li>
import VueJwtDecode from "vue-jwt-decode";
export default {
  data() {
    return {
      user: {}
  methods: {
    getUserDetails() {
      // get token from localstorage
      let token = localStorage.getItem("user");
      try {
      //decode token here and attach to the user object
      let decoded = VueJwtDecode.decode(token);
      this.user = decoded;    
      } catch (error) {
        // return error in production env
        console.log(error, 'error from decoding token')
    logUserOut() {
  created() {
#navbar {
  margin-bottom: 15px;

Because the vue-jwt-decode package is lightweight, we need to handle error cases on our end by wrapping it in a try/catch block. Additionally, you can find details about the route file and other files needed for the entire client app setup in the client folder on GitHub.

Deploying our application to Heroku

If you want to run the application locally, change the database settings in the .env to match your local Postgres database, then run npm run create-dev-tables, which will create the tables you need to run the code locally without the Heroku CLI.

To deploy our app to Heroku, we first need a Heroku account and the Heroku CLI installed on our dev machine. Visit the documentation for instructions. After we’re done specifying settings like the Node engine version and the start script, we can then go ahead and test our app locally.

To do so, we need a Procfile in the root of our project, which Heroku will use to tell our app the path of the server to run. The content of the file is shown below:

web: node ./dist/server.js

To start our app locally, we can run heroku local web. The output is shown below:

[OKAY] Loaded ENV .env File as KEY=VALUE Format
01:43:05 web.1   |  postgres://kdwqywwonzxzoa:8[email protected] DATABASE_URL
01:43:05 web.1   |  3000 APP-PORT
01:43:05 web.1   |  App live and listening on port: 3000

As we can see, we’ve set up a Postgres add-on on the Heroku dashboard because we’re using a Postgres database for our app. Additionally, looking at the script section of our package.json file, we can see the parts needed by our app for production.

The build command runs "rm -rf dist && mkdir dist && npm run build-server", which deletes any old dist folders, creates a dist directory, and builds our app. The build-server command runs "babel server -d dist". The final start script can be found in the script section of the same file as above.

Finally, we just need to deploy our app to Heroku. To do so, we should ensure to commit our changes to Git on the master branch, then use the command heroku login on the Heroku CLI to log into our Heroku account. Once we enter the correct credentials, we can create a new Heroku app with the heroku create command.

Next, we can push our app to the Heroku master branch by running git push heroku master. We can then see the URL to our deployment, which we can find in a truncated portion of the output shown below:

remote:        found 0 vulnerabilities
remote: -----> Build succeeded!
remote: -----> Discovering process types
remote:        Default types for buildpack -> web
remote: -----> Compressing...
remote:        Done: 36.8M
remote: -----> Launching...
remote:        Released v5
remote: deployed to Heroku
remote: Verifying deploy... done.
   91ced12..28baa4c  master -> master

Finally, copy the URL, navigate to the src/main.js folder of our client app, and update the base URL. The final content of that file is shown below:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import axios from "axios";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
const base = axios.create({
  baseURL: "" // replace on production env
import store from "./store/index";
Vue.config.productionTip = false;
Vue.prototype.$http = base;
new Vue({
  render: h => h(App)

Note the part where we added the baseURL with the axios.create method. Afterwards, we can commit these new changes again to Git, and we’re live! Finally, to test our app, we can navigate to the login route and test our deployment. The path is shown below:

Once we register a new account, we can then log in, and we’ll be immediately redirected to the home route:

Redirect Home Route Login

Our app’s homepage is shown below:

App Homepage JWT

Once we are redirected to the homepage, for demonstration purposes, we log the JWT payload of the currently authenticated user to the console. The details are shown in the image below:

JWT Payload


In this tutorial, we’ve learned how to integrate JWT in a Node.js and Vue app. We started with the entire backend setup, built our app, and then worked on the client-side of the application.

JWT offers a purely stateless authentication mechanism because the user state is never saved in the server memory or database. The server’s protected routes will check for a valid JWT in the authorization header, and if there is one, the user will be granted access.

We’ve reviewed how JWTs work and their structure. We’ve also learned how to generate JWTs and verify JWT data. On the client layer, we’ve learned how to decode the JWT using the vue-jwt-decode library.

Finally, we should note that storing tokens in localStorage comes with its own set of risks since they are vulnerable to cross-site scripting (XSS) attacks. It’s essential to review the details about preventing security issues with JWTs so you’ll know when to use an alternative.

For more details on JWTs, refer to this detailed handbook and the documentation. Please reach out in the comment section for questions and feedback. Thanks for reading along!

Experience your Vue apps exactly how a user does

Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Vue apps - .

Alexander Nnakwue Software engineer. React, Node.js, Python, and other developer tools and libraries.

Leave a Reply