MEVN Stack Boilerplate — Vue 3, Nuxt 3, Express, Vuetify
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.
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