MEVN Stack Boilerplate — Vue 3, Nuxt 3, Express.js, Node.js, Vuetify, Pinia

Mustafa Çağrı Güven
8 min readFeb 4, 2023

--

Hey there, in this blog post, we will talk about the MEVN Stack Boilerplate that I published on GitHub some days ago. If you are interested in Vue 3, Nuxt 3, Node.js, Express.js, or MongoDB technologies, you may make a quick start with this boilerplate. Because of its features/structure and low learning curve, I think it will provide you with an important contribution.

In addition, if you have any features that you think would be nice to add, please don’t hesitate to open a PR and contribute to the repository. Also, if you come across a bug, you can open an issue related to the subject.

Let’s start by discussing the main interesting parts of this boilerplate:

  • MongoDB
  • Express
  • Vue 3 (Admin Dashboard)
  • Node.js
  • + Nuxt3 (Client)

Furthermore, this boilerplate includes Vuetify and Vuexy in its admin dashboard. Moreover, I used Pinia, which was introduced into our lives with Vue 3, for store management. Besides these, a few other technologies used are Multer, Slug, Lodash, and more.

MENV Stack Boilerplate — Vue 3 - Vuext Admin Dashboard

https://github.com/mustafacagri/mevn-boilerplate/assets/7488394/aca14f0a-af96-4019-8be2-ad9d13f3e9d7s

The video was added on September 24, 2023

In the dashboard, I chose vuetify which I have been actively using for over two years. However, to include better structures in the basic elements of vuetify, I chose the vuexy theme based on vuetify.

Since it’s a classic Vue 3 application, I won’t go into the details of it at this time. However, if you haven’t met Vue 3 yet and are curious about how it is, it will provide you with an important contribution to your learning. For example, some improvements that seem simple, such as using <script setup> without an unnecessary container (such as <div>, etc…) in the template, using variables directly in the template without returning them in the setup, and the project starting up in less than 1-second thanks to Vite, make Vue 3 more developer-friendly.

You will also find active use of pinia both on the admin side (Vue3) and the client (Nuxt3) side.

API — Node.js & Express.js

On the backend side, we use this amazing duo (Node.js & Express.js).

Mevn Stack Boilerplate — Server Folder Structure

Gradually, we may start talking about what this boilerplate is.

MEVN Boilerplate — Express Helper Classes

Classes

In this folder, we have helper classes/functions that I usually prefer to use in classes.

UrlGenerator.js

When creating a record in the database, we want to fill the URL / slug column. However, what if that URL has been used before? If we add a repeated URL, we will not be able to access the first or last record. Therefore, with the help of this helper class, we check whether the needed URL is already in use in the related table/collections. If it is in use, we add numbers (up to 10) to the end of it.

For example, we want to create a record with the URL “first-post”. But if “first-post-1” and “first-post-2” have already been recorded, we give the new record the URL “first-post-3”. If all the URLs up to “first-post-10” are already in use, we give the new record the URL “first-post-1674932835” (timestamp). The goal here is to create URLs that are SEO-friendly.

const slug = require('slug')

async function UrlGenerator(url, document) {
url = slug(url) // let's slug it before we start
const currentDoc = require(`../../models/${document}`)
let tempUrl = url
cur = 0
check = false

while (!check && cur < 10) {
const data = await currentDoc.findOne({ url: tempUrl })

if (!data) {
check = true
} else {
cur++
tempUrl = `${url}-${cur}`
}
}

if (cur === 10) {
const d = new Date()
tempUrl += '-' + d.getTime() // if we try it more than 10, we will give up to find an url and we will only add the timestamp suffix
}

url = tempUrl

return url
}

module.exports = UrlGenerator

Now let’s take a look at the response folder. With the files in this folder, we ensure that the returns we make from our API to the front end are in certain patterns. You will also see that it brings us ease and speed.

dynamic.js

With dynamic.js, we have the opportunity to only return certain fields within the controller. For example, we need to return only certain fields related to the user (of course, we don’t want to return the user’s password, right?), in this case, we can create new data with a map. But, with this structure, we can quickly perform the return process.

An example usage:

res.json(new response.dynamic([‘_id’, ‘username’, ‘email’, ‘roles’], res.user))
// this function only works for objects not arrays
// for arrays, you need to use "map"

const success = require('./success')
const fail = require('./fail')
const ErrorResponse = require('../../utils/errorResponse')

function dynamic(list, data) {
const arr = {}
if (list && data) {
list.forEach(item => {
arr[item] = data[item]
})
// here we are only returning the necessary fields which is coming from with "list param"

return new success(arr)
} else {
return new fail('No data found')
}
}

module.exports = dynamic

fail.js

The fail.js file helps us understand that the response from the API was incorrect on the front end. There are two properties for this purpose: isSuccess and message.

We can understand that the response was incorrect with isSuccess. And we can send our message to the front end with the message.

We also log this through the logger.

Here, we take advantage of the built-in error types in JS. For more information, you can check out the Mozilla developer page.

const logger = require('../../utils/logger')
class FailedResponse extends Error {
constructor(message) {
// super(message)
super()
this.message = message
this.isSuccess = false
logger.error(`Error.Name: ${this.name} - Error.Stack: ${this.stack}`)
}
}

module.exports = FailedResponse

failed.js

This is a shortened version of fail. In the case of returns that we already know to be incorrect, only sending the message is sufficient.

success.js

Here we convey our successful returns to the front end.

class Response {
constructor(data, count = null, pagination = null, message = null) {
this.isSuccess = true
this.data = data
this.count = count
this.message = message
this.pagination = pagination
}
}

module.exports = Response

In this, there are properties like data, count, pagination, etc.

data: as the name suggests, we transfer our data. count: we inform how many records there are. Thanks to this, in some cases, we can directly show the record count on the screen without having to use data.length extra. pagination: this can be our most important property. Because of this, we can show previous, next, etc. pages on the front end.

And with dbQuery.js, which we will talk about soon, we can quickly return data/count/pagination from the routes.

successed.js

Just like failed.js, it’s a convenient shortcut.

user / mail.js

For now, we only have a validateEmail function in this structure.

Yes, we can summarize our classes folder in this way. Let’s move on to our next folder.

Config

auth.js: This is our file for database authentication operations. Our authentication secret is located here.

db.js: This is where we establish our connection with MongoDB.

Controllers

There are currently two different controllers available.

Admin

We don’t want the data we will use in our admin dashboard to be accessible from the client, do we? Therefore, only those with administrator tokens can access the data here.

In the controllers in the main controllers folder (with some exceptions, such as membership requirements, etc.), we aim for everyone to be able to access them.

Middlewares

We create these middlewares to avoid duplications in structures that will be needed multiple times. I would like to specifically mention two middlewares.

dbQuery.js

With dbQuery.js, we can dynamically fetch data without needing any controllers in the router. Thanks to this middleware and response.success, we can easily return data, count, and pagination.

This is a part that could be explained in much more detail, however, I will briefly mention it for now. However, you can see various examples within the routers as well.

const { response } = require('../../classes')

const dbQuery =
(model, populate = [], willNotReturn = []) =>
async (req, res, next) => {
const CONTANTS = { limitPerPage: 25, defaultPage: 1 }

let query
willNotReturn.push('__v')

// Copy req.query
const reqQuery = { ...req.query }

// Fields to exclude
const removeFields = ['select', 'sort', 'page', 'limit']

// Loop over removeFields and delete them from reqQuery
removeFields.forEach(param => delete reqQuery[param])

// Create query string
let queryStr = JSON.stringify(reqQuery)

// Create operators ($gt, $gte, etc)
queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`)

// Finding resource
query = model.find(JSON.parse(queryStr))

// Select Fields
if (req.query.select) {
const fields = req.query.select.split(',').join(' ')
query = query.select(fields)
}

// Sort
if (req.query.sort) {
const sortBy = req.query.sort.split(',').join(' ')
query = query.sort(sortBy)
} else {
query = query.sort('-createdAt')
}

// Pagination
const page = parseInt(req.query.page, 10) || CONTANTS.defaultPage
const limit = parseInt(req.query.limit, 10) || CONTANTS.limitPerPage
const startIndex = (page - 1) * limit
const endIndex = page * limit
const total = await model.countDocuments(JSON.parse(queryStr))

query = query.skip(startIndex).limit(limit)

if (populate.length > 0) {
populate.forEach(p => {
query = query.populate(p)
})
}

// // Executing query
let results = await query

results = JSON.stringify(results)
results = JSON.parse(results)
// map does not with only results.map if we do not stringify then parse it. strange, right? any idea?

results.map(item => {
return willNotReturn.forEach(willBeDeleted => delete item[willBeDeleted])
})

// Pagination result
const pagination = {}

if (endIndex < total) {
pagination.next = {
page: page + 1,
limit
}
}

if (startIndex > 0) {
pagination.prev = {
page: page - 1,
limit
}
}

res.dbQuery = new response.success(results, results.length, pagination)

next()
}

module.exports = dbQuery

error.js

As its name suggests, it helps us to easily overcome errors.

Models

Our models folder — contains our table/collection structures.

Public

The folder contains images that we will only use in our posts for now.

Routes

Our folder contains our routes. Everything mentioned in the Controllers section is also applicable here.

Utils

The folder contains tools that can make our job easier. Currently, it holds the errorResponse.js and logger.js files.

Yes, the API side is summarized as follows.

The client-side prepared with Nuxt 3 looks as follows. I didn’t use any UI framework like vuetify. It can be seen as a Nuxt 3 project consisting of only basic CSS (I also added a part of bootstrap’s CSS directly to avoid too much effort in responsiveness).

The footer comes from a Vue JS Footer project that I shared a few years ago on GitHub.

Also, I will discuss the Client and Admin sides over time. You can also take a look at the repo for more detailed usage information.

https://github.com/mustafacagri/mevn-boilerplate

As I mentioned earlier, feel free to contribute to the repo for any detail that comes to your mind. Such as creating PRs or Issues… You may also contribute by giving stars to the repo.

Let’s grow the open-source world together…

Open Source ❤

--

--

Mustafa Çağrı Güven
Mustafa Çağrı Güven

Written by Mustafa Çağrı Güven

Comp. Eng. @Sabancı University Graduated '11 / Senior Frontend Wizard / Vue.js 3 / Node.js / Express.js / MEVN / Nuxt 3 / Clean Code & Open Source ❤