MEVN Stack Boilerplate — Vue 3, Nuxt 3, Express, Vuetify

Mustafa Çağrı Güven
7 min readJan 29, 2023

--

Selamlar, bu yazıda geçtiğimiz günlerde Github’da yayınladığım MEVN Stack Boilerplate konusuna değineceğim. Eğer vue 3, nuxt 3, node.js, express.js, mongodb teknolojilerine ilgi duyuyorsanız, bu boilerplate ile hızlı bir başlangıç yapabilirsiniz. Basit olmaktan uzak özellikleri / yapısı ve de öğrenme eşiğinin bir hayli düşük olması sebebiyle, size önemli bir katkı sağlayacağını düşünüyorum.

Bunun yanı sıra, eğer eklenmesinin güzel olacağını düşündüğünüz özellikler varsa da lütfen PR açmaktan ve repoya katkı sağlamaktan çekinmeyiniz. Ayrıca, gözünüze çarpan bir hata olursa da konuyla alakalı bir Issue açabilirsiniz.

Bu boilerplate’in başlıca ilgi çeken kısımlarına ise hemen değinmeye başlayabiliriz:

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

Bunların yanı sırada bu boilerplate’in admin dashboard’unda vuetify ve vuexy yer almaktadır. Ayrıca, store management olarak da vue 3 ile hayatımıza giren pinia’yı kullandım. Bunların yanı sıra kullanılan diğer teknolojilerden birkaçı ise: multer, slug, lodash…

Dashboard’da 2 seneyi geçen bir süredir aktif olarak kullandığım vuetify’ı tercih ettim. Ancak, vuetify’ın temel elementlerine daha güzel yapılar dahil etmek için de vuetify’ı baz alan vuexy temasını tercih ettim.

Klasik bir Vue 3 uygulaması olduğu için şu anda bunun detaylarına çok fazla girmeyeceğim. Ancak, Vue 3 ile henüz tanışmadıysanız ve nasıl olduğunu merak ediyorsanız, öğrenmenize önemli bir katkı sağlayacaktır. Öyle ki, bazı basit gibi görünsede yapılan iyileştirmeler Vue 3'ü daha kullanışlı kılıyor. Örneğin; <script setup>, template içesisinde gereksiz bir container olmadan (<div> vs..) kullanabilme, setup içerisinde tanımladığınız değişkenleri return etmeden template içerisinde direkt olarak kullanabilme, vite sayesinde projenin 1 saniyeden daha kısa sürede ayağa kalması…

Ayrıca, pinia’nın aktif olarak kullanımı da hem admin tarafında (Vue3) hem de client (Nuxt3) tarafında bulabilirsiniz.

API — Node.js & Express.js

Backend tarafında bu muhteşem ikiliyi (Node.js & Express.js) kullanıyoruz.

Yavaş yavaş, bu boilerplate’in nelere sahip olduğuna değinmeye başlayabiliriz.

Classes

Bu klasör içerisinde utils’e de girebilecek ancak benim genellikle classes içerisinde kullanmayı tercih ettiğim yardımcı class’larımız / fonksiyonlarımız yer alıyor.

UrlGenerator.js

Veritabanına bir kayıt açarken, url / slug sütununu doldurmak istiyoruz. Peki, daha önceden bu url kullanıyorsa ne yapacağız? Tekrarlanan bir url eklersek, sonradan / ilk oluşan kayda ulaşamayacağımız aşikar. Bu nedenle de bu yardımcı class aracılığıyla ilgili tabloya / collections’a giderek, daha önceden ihtiyacımız olan url kullanılıyor mu, kontrol ediyoruz. Eğer kullanıyorsa, sırasıyla sonuna (10'a kadar) rakamlar ekliyoruz.

Örneğin: first-post url’sine sahip bir kayıt oluşturmak istiyoruz. Ama daha önceden first-post-1 ve first-post-2 kayıtlıysa, biz bu yeni kaydımıza first-post-3 url’sini veriyoruz. Eğer ki first-post-10'a kadar tüm url’ler doluysa da first-post-1674932835 (timestamp) url’sini veriyoruz. Buradaki amaç seo uyumulu url’ler oluşturmak.

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

Şimdi de response klasörüne göz atalım. Bu klasördeki dosyalar sayesinde API’ımızdan front end’e yaptığımız return’lerin belli kalıplarda olmasını sağlıyoruz. Ayrıca, bize kolaylık ve hız kazandırdığını da göreceksiniz.

dynamic.js

dynamic.js ile controller içerisinde sadece belli field’ları return edebilme olanağına kavuşuyoruz. Örneğin, userla alakalı sadece belli field’ları dönmemiz gerekiyor (elbette kullanıcının şifresini dönmek istemeyiz değil mi?), bu durumda map ile yeni bir data oluşturabiliriz. Ancak, bu yapı sayesinde return işlemini hızlıca yapabiliyoruz.

Örnek bir kullanım:

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

Front end tarafında, API’dan gelen sonucun hatalı olduğunu anlamamıza yarıyor. Bunu anlayabilmek için iki tane property’miz mevcut. isSuccess ve message.

isSuccess ile dönüşümüzün hatalı olduğunu anlıyoruz. message ile de mesajımızı front end’e iletiyoruz.

Ayrıca, logger vasıtasıyla da bunu log’luyoruz.

Burada JS’in built-in error türlerinden faydalanıyoruz. Daha detaylı bilgi için Mozilla’nın developer sayfasına göz atabilirsiniz.

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

Üstteki fail’in biraz daha kısaltmış hali. Hali hazırda hatalı olduğunu bildiğimiz dönüşler için sadece mesajı iletmemiz yeterli oluyor.

success.js

Burada başarılı dönüşlerimizi önyüze iletiyoruz.

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

Burada farklı olarak data, count, pagination gibi property’lerimiz mevcut.

data: isminden de anlaşılacağı üzere datamızı iletiyoruz.
count: kaç tane kaydın bulunduğu bilgisini iletiyoruz. Bunun sayesinde bazı durumlarda ekrana direkt olarak kayıt sayısını basabiliriz, ekstra olarak data.length kullanmamıza da gerek kalmaz.
pagination: bu ise en önemli property’imiz olabilir. Çünkü bunun sayesinde front end’de önceki sonraki gibi sayfaları gösterebiliriz.

Az sonra değineceğim dbQuery.js ile de, hızlı bir şekilde route’lardan data / count / pagination dönebileceğiz.

successed.js

failed.js’de olduğu gibi kullanışlı bir kısa yol.

user / mail.js

Şimdilik sadece validateEmail fonksiyonuna sahip olan yapımız.

Evet, classes klasörümüzü bu şekilde özetleyebiliriz. Gelelim bir sonraki klasörümüze.

Config

auth.js: veritabanına authentication işlemlerimizin yapıldığı dosyamız. İçerisinde authentication secret’imiz yer alıyor.

db.js içerisinde ise mongodb ile olan bağlantımızı sağlıyoruz.

Controllers

Şu an için 2 farklı controller’ımız mevcut.

admin

Admin dashboard’umuzda kullanacağımız datalara, client’tan ulaşılmasını istemeyiz değil mi? Bu nedenle de sadece yönetici olan kişilerin tokenlarıyla ulaşabilecekleri veriler burada yer alıyor.

Controllers ana klasöründeki controller’lara ise (bazı durumlar hariç: üyelik şartı vs.. gibi) herkesin ulaşabilmesini amaçlıyoruz.

middlewares

Buradaki middleware’ler sayesinde de birden fazla kez kullanmamız gerekecek yapıları, duplicationları önlemek için oluşturuyoruz. Özellikle 2 middleware’e değinmek istiyorum.

dbQuery.js

dbQuery.js sayesinde router’da hiçbir controller’a ihtiyaç duymadan dinamik olarak data çekebiliyoruz. Bu middleware ve response.success sayesinde data’yı, count’u, pagination’u kolaylıkla dönebiliyoruz.

Çok daha detaylı anlatılabilecek bir kısım olmasına rağmen şimdilik hızlıca değinmiş oluyorum. Ancak, router’lar içerisinde de çeşitli örnekleri görmek mümkün olacak.

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

İsminden de anlaşılacağı üzere hataların üstesinden kolaylıkla gelmemize yarıyor.

Models

Modellerimizi — tablo / collection yapılarımızı barındıran klasörümüz.

Public

Şu an için sadece postlarımızda kullanacağımızı görselleri barındıran klasörümüz.

Routes

Route’larımızın yer aldığı klasörümüz. Controllers başlığında değindiğim konuların hepsi burada da geçerli.

Utils

İşimizi kolaylaştırabilecek araçların yer aldığı klasörümüz. Şimdilik errorResponse.js ve logger.js dosyalarını barındırıyor.

Evet, API tarafı özet olarak bu şekilde görünüyor.

Nuxt 3 ile Client

Nuxt3 ile hazırlanan client tarafı ise aşağıdaki gibi görünüyor. Bunun için vuetify benzeri herhangi bir UI framawork kullanmadım. Sadece temel css’lerden (responsive konusunda çok uğraşmamak için direkt olarak bootstrap’in css’lerinin bir kısmını da ekledim) oluşan bir Nuxt 3 projesi olarak görebilirsiniz.

Footer ise birkaç yıl önce paylaştığım Vue JS Footer projesinden geliyor.

MEVN Stack Boilerplate — Client Nuxt 3

Ayrıca, Client ve Admin taraflarına da zaman içerisinde değineceğim. Daha detaylı kullanım bilgileri için de repoya göz atabilirsiniz.

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

Başta da belirttiğim gibi aklınıza gelen her detay için repoya katkı sağlamaktan lütfen çekinmeyin. Bunun yanı sıra, repoya yıldız vererek de katkı sağlayabilirsiniz. Bilgi paylaştıkça çoğalır…

Open Source ❤

Selamlarımla

--

--

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 ❤