In this tutorial, we will create a MEVN stack CRUD application using MongoDB, Express, Vue 3, and Node.js. In our Node.js API, we will be able to perform create, read, update, and delete operations.

At the end of the tutorial, readers should be able to create a CRUD app with Node.js, connect the Node.js server to the backend, and consume the API with the Vue 3 frontend.

Building the MEVN CRUD App

First, we create our project folder, name it quote-app , then in our quote-app directory, we create our backend folder, name it backend. In our backend folder, we run

npm init -y

initializing our package.json file.

Installing dependencies

For our server, we need to install Express, Bodyparser, mongoose, and Cors.

npm install express body-parser mongoose cors --save

Install Nodemon

yarn add -D nodemon

And in the package.json script add

"start": "nodemon app.js"

In our backend folder, we create a file named app.js and in that file, we require express, mongoose, and bodyparser.
we initialize our express app then listen at port 3000 for any incoming request.

const express = require("express")
const mongoose = require("mongoose")
const bodyParser = require("body-parser")
// create our express app
const app = express()
// middleware
app.use(bodyParser.json())
// routes
app.get("/", (req,res)=>{
    res.send("my home page dey show sha")
})
//start server
app.listen(3000, ()=>{
    console.log("listeniing at port:3000")
}) 

Connecting our database

We are going to be using MongoDB atlas instead of downloading it locally. You first have to create an account on their site. Then create a cluster, choose the free tier cluster, then create the cluster.

You can choose to change the cluster name. Click on Build Database, and you will be taken to another page. On this page, under your cluster name, click on the connect button and follow the steps and you will get your connection string that can be used to connect to the application.

In our app.js after our dependencies have been required, copy and paste the following code to connect our database.

const uri = "mongodb+srv://<cluster-username>:<user-password>@quote-app.ba0sr.mongodb.net/myFirstDatabase?retryWrites=true&w=majority";
mongoose.connect(uri, {
    useNewUrlParser: true,
    useUnifiedTopology: true
  })
  .then(() => {
    console.log('MongoDB Connected')
  })
  .catch(err => console.log(err))

In place of <cluster-username>should be the name of the user you set for that cluster and in place of <user-password> should be the password for the user.
Now if we start our server with npm start we would see the message MongoDB Connected.

Creating our routes

Let’s create our quotes route first by creating a route folder in our root directory, then in the route folder we create a Quotes.js file for our Quote route, then back in our, we require the quote route and use it. All routes created in our Quote route will now have /Quote prepended to it.

const QuotesRoute = require('./routes/Quotes')
app.use('/quotes', QuotesRoute)

In our Quote.js file, we require to express again and define our express router. we also create an all-quotes route that fetches all our quotes.

const express = require('express')
const router = express.Router()

// all routes
router.get('/', (req,res)=>{
    res.send("Our quotes route")
})

The '/' route will automatically default to the /quotes route we setup to be used by Express in app.js file.

The post route is then created

router.post('/new', (req,res)=>{
    res.send('posted info')
})

but if we are posting to the route specified above we need to create a model in our database. We create a folder at the root of our app named models and in it, we create a file Quotes.js to create our model.

In the Quotes.js file, we import Mongoose again and create our schema.

Schemas are like rules as to what we can store in our database collection. So we create our quotes schema and pass it some rules, setting content and author to be strings. Exporting our model and giving it the name of quotes

const mongoose = require('mongoose')
const QuotesSchema = new mongoose.Schema({
    content: String,
    author: String,
})
module.exports = mongoose.model('quote', QuotesSchema)

We import the Mongo schema we just created at the top of our Quotes.js file in our route folder.

const Quote = require('../models/Quotes');

Create a new quote.

In our post route in the routes folder, we create a new quote and pass in the body content using req.body to be stored in the database. Then we save the data in our database and respond with JSON to our post endpoint.
Note: You can use Postman to make sure all routes are working.

router.post('/new', async(req,res)=>{
    const newQuote = new Quote(req.body);
    const savedQuote = await newQuote.save()
    res.json(savedQuote)
})

we use async/await because saving data to a database is an operation that takes some time and we don’t want our code to be blocked.
We create a newQuote variable containing our new quote and use mongoDB save() method to save our quote

Get specific quote

When we create a new post and save it in our database, it is assigned an id to uniquely identify the quote saved. We can create a route that gets a quote by its id

// Get specific quote
router.get('/get/:id', async (req, res) => {
    const q = await Quote.findById({ _id: req.params.id });
    res.json(q);
});

Using the get request, we make a request to the get route that has a dynamic :id prepended to it. MongoDB findById method takes in the id as an argument and finds any quote that has the id and returns it, saving it to a variable q, then we respond with json.

Get random quote

To get a random quote from our saved quotes, we use Mongo countDocuments method to calculate the number of quotes we have, then we create the random variable using findOne().skip(random) to return random quotes in our saved quotes.

// Get random quote
router.get('/random', async (req, res) => {
    const count = await Quote.countDocuments();
    const random = Math.floor(Math.random() * count);
    const q = await Quote.findOne().skip(random);
    res.json(q);
});

Update and delete the quote

We have made use of the get and post request so far, we would now see how to delete and update a request using update and delete request.

We create a delete route containing the dynamic id. We then use Mongo findByIdAndDelete method to find the quote id and delete it when found.

Then to update a quote we have already saved, we use the patch function and updateOne method to update the quote specified.

// Delete a quote
router.delete('/delete/:id', async (req, res) => {
    const result = await Quote.findByIdAndDelete({ _id: req.params.id });
    res.json(result);
});
// Update a quote
router.patch('/update/:id', async (req, res) => {
    const q = await Quote.updateOne({_id: req.params.id}, {$set: req.body});
    res.json(q);
});

Now we are done with creating all our routes. We can now start building the front end.

Start the Server

Run the following command to start the backend server:

npm start
MEVN CRUD backend
MongoDB Connect

Setting up our frontend

You can start a new project using the vue cli but you have to install the CLI first by running-

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

after the cli is installed you can create a new project by running-

vue create <project-name>

you will then be prompted to select some basic setup. In your setup, select Vue router, store, and vue3.
You can set up Tailwind by following the official documentation here.

Installing dependencies

For our Frontend, we need to install Axios, Pinia, and Vue-Router.

npm install axios pinia vue-router

Set up the Pinia store

We are going to use the Pinia store for managing quotes. Pinia is a state management library for Vue.js. It aims to provide a simpler and more straightforward API than Vuex while also providing more flexibility.

In the Stores/quotes.js file add the following code:

import { defineStore } from 'pinia';
import axios from 'axios';

const API_URL = 'http://localhost:3000/quotes';

export const useQuotesStore = defineStore({
  id: 'quotes',
  state: () => ({
    quotes: [],
    currentQuote: null,
  }),
  actions: {
    async fetchQuotes() {
      const response = await axios.get(API_URL);
      this.quotes = response.data;
    },
    async fetchQuote(id) {
      const response = await axios.get(`${API_URL}/get/${id}`);
      this.currentQuote = response.data;
    },
    async addQuote(quote) {
      await axios.post(`${API_URL}/new`, quote);
      await this.fetchQuotes();
    },
    async updateQuote(quote) {
      await axios.patch(`${API_URL}/update/${quote._id}`, quote);
      await this.fetchQuotes();
    },
    
    async deleteQuote(id) {
      await axios.delete(`${API_URL}/delete/${id}`);
      this.quotes = this.quotes.filter((quote) => quote._id !== id);
      if (this.currentQuote?._id === id) {
        this.currentQuote = null;
      }
    },
  },
});

Here is what each part of the code does:

  • const API_URL = 'http://localhost:3000/quotes';: This sets the base URL for the API that the store will interact with.
  • export const useQuotesStore = defineStore({ ... });: This exports a function that can be used to access the quotes store. The store is defined by passing an object to defineStore.
  • id: 'quotes',: This sets the unique ID of the store. This is used by Pinia to ensure that there is only one instance of each store.
  • state: () => ({ quotes: [], currentQuote: null, }),: This defines the state of the store. The state includes an array of quotes and a currentQuote which is initially null.
  • actions: { ... },: This defines the actions of the store. Actions are methods that can be used to modify the state.

In summary, this code defines a Pinia store that fetches, creates, updates, and deletes quotes from an API. It uses axios for making HTTP requests to the API.

Set up our Routes

So in our application, we have four views

  • Home.Vue
  • NewQuote.vue
  • QuoteDetails.Vue
  • EditQuote.Vue

In our Views folder, we make sure to create all the views we just listed above.
So our routes in the index.js of our router folder look like this:

import { createRouter, createWebHistory } from 'vue-router';
import Home from "../views/Home.vue";
import QuoteDetails from '../views/QuoteDetails.vue';
import EditQuote from '../views/EditQuote.vue';
import NewQuote from '../views/NewQuote.vue';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/quote/:id',
    name: 'QuoteDetails',
    component: QuoteDetails,
    props: true,
  },
  {
    path: '/edit/:id',
    name: 'EditQuote',
    component: EditQuote,
    props: true,
  },

  {
    path: '/add',
    name: 'NewQuote',
    component: NewQuote,
    props: true,
  },

  
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

We first defined The routes array & each object in this array represents a route in the application. A route object typically includes:

  1. path: This is the URL path for the route.
  2. name: A name for the route, which can be used to refer to this route in other parts of the code.
  3. component: The Vue component that should be rendered when a user navigates to this route.
  4. props: Whether or not the route should receive the route parameters as props.

The route objects in the routes array define the following routes:

  • Home page at the root path ('/').
  • Quote details page ('/quote/:id'), where ':id' is a dynamic segment that will match any value.
  • Edit quote page ('/edit/:id'), similar to the quote details page but for editing a quote.
  • New quote page ('/add') for adding a new quote.

The createRouter function is then called with an object that configures the router.

The history option is set to createWebHistory(), which means the router will use the HTML5 history API for changing the URL without refreshing the page. The routes option is set to the routes array defined earlier.

Finally, the router object created by createRouter is exported so it can be used in other parts of the application.

Set Up the App.Vue

<template>
  <div id="app" class="flex flex-col min-h-screen bg-gray-100">
    <header class="bg-white shadow py-4 px-6 flex justify-between items-center">
      <h1 class="text-2xl font-semibold text-blue-600">MEVN CRUD Quotes APP</h1>
      <nav class="flex space-x-4">
        <router-link
          to="/"
          class="text-blue-600 hover:text-blue-800 font-medium text-lg"
        >
          Home
        </router-link>
        <router-link
          to="/add"
          class="text-blue-600 hover:text-blue-800 font-medium text-lg"
        >
          Add Quote
        </router-link>
      </nav>
    </header>
    <main class="flex-grow">
      <router-view></router-view>
    </main>
  </div>
</template>

<script>
export default {
  name: 'App',
};
</script>

The app.vue defines the main App component, which serves as the layout for the application. we included a navigation bar with links to the home page and the add quote page, and a router view area where the content for the current route is displayed.

Setup the Views

Now, Let’s move Forward and set up the Views for our app.

Home.Vue

Mevn CRUD
Home.vue
<template>
  <div class="bg-gray-100 min-h-screen">
    <div class="container mx-auto px-4 py-8">
      <h1 class="text-4xl font-bold text-center text-blue-600 mb-8">Quotes</h1>
      <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
        <QuoteCard
          v-for="quote in quotes"
          :key="quote._id"
          :quote="quote"
          class="bg-white rounded-lg shadow-md p-6"
        >
          <blockquote class="text-xl italic font-serif mb-4 leading-relaxed">
            <q>{{quote.text}}</q>
          </blockquote>
          <footer class="text-right text-gray-700 font-medium">
            - {{quote.author}}
          </footer>
        </QuoteCard>
      </div>
    </div>
  </div>
</template>

<style>
@import url("https://fonts.googleapis.com/css2?family=Merriweather&display=swap");

blockquote {
  font-family: 'Merriweather', serif;
  position: relative;
  padding-left: 1.5rem;
}

blockquote::before {
  content: open-quote;
  position: absolute;
  left: 0;
  top: -0.25rem;
  font-size: 3rem;
  font-weight: bold;
  color: #e5e7eb;
}
</style>


<script>
import { useQuotesStore } from '../stores/quotes';
import QuoteCard from '../components/QuoteCard.vue';

export default {
  components: {
    QuoteCard,
  },
  async created() {
    const quotesStore = useQuotesStore();
    await quotesStore.fetchQuotes();
  },
  computed: {
    quotes() {
      const quotesStore = useQuotesStore();
      return quotesStore.quotes;
    },
  },
};
</script>

Home.Vue displays a list of quotes. Then It fetches all quotes when the component is created and provides a computed property that gets the current list of quotes from the store.

NewQuote.Vue

NewQuote.Vue
<template>
  <div class="container mx-auto p-4">
    <h1 class="text-4xl font-semibold mb-4">Add a new quote</h1>
    <form @submit.prevent="submitQuote" class="bg-white rounded-lg shadow-md p-6">
      <div class="mb-4">
        <label for="content" class="block text-gray-700 font-medium">Quote:</label>
        <input
          v-model="content"
          type="text"
          id="content"
          class="block w-full mt-1 bg-gray-100 border border-gray-300 rounded p-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
        />
      </div>
      <div class="mb-4">
        <label for="author" class="block text-gray-700 font-medium">Author:</label>
        <input
          v-model="author"
          type="text"
          id="author"
          class="block w-full mt-1 bg-gray-100 border border-gray-300 rounded p-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
        />
      </div>
      <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
        Submit
      </button>
    </form>
  </div>
</template>

  
  <script>
  import { useQuotesStore } from '../stores/quotes';
  
  export default {
    data() {
      return {
        content: '',
        author: '',
      };
    },
    methods: {
      async submitQuote() {
        const quotesStore = useQuotesStore();
        await quotesStore.addQuote({ content: this.content, author: this.author });
        this.$router.push('/');
      },
    },
  };
  </script>

NewQuote.vue allows users to create a new quote.

It has a local state for the quote’s content and author, and a method that submits the new quote to the store and then redirects to the home page.

QuoteDetails.vue

QuoteDetails.vue
<template>
    <div class="container mx-auto p-4">
      <h1 class="text-4xl font-semibold mb-4">{{ quote.content }}</h1>
      <h2 class="text-2xl font-semibold mb-4">- {{ quote.author }}</h2>
      <div class="flex space-x-4">
        <router-link to="/">Go back</router-link>
        <router-link
          :to="`/edit/${quote._id}`"
          class="text-blue-500 hover:text-blue-700"
        >
          Edit
        </router-link>
        <button
          @click="deleteCurrentQuote"
          class="text-red-500 hover:text-red-700"
  >
    Delete
  </button>
</div>

  
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { useQuotesStore } from '../stores/quotes';
import { useRouter } from 'vue-router';

export default {
  setup(props, { emit }) {
    const quote = ref({});
    const router = useRouter();
    const quotesStore = useQuotesStore();
    const routeId = router.currentRoute.value.params.id;

    const fetchQuote = async () => {
      await quotesStore.fetchQuote(routeId);
      quote.value = quotesStore.currentQuote;
    };

    const deleteCurrentQuote = async () => {
      await quotesStore.deleteQuote(routeId);
      router.push('/');
    };

    const updateOne = async () => {
      await quotesStore.updateOneQuote(routeId);
      router.push('/');
    };

    onMounted(fetchQuote);

    return { quote, deleteCurrentQuote, updateOne };
  },
};
</script>

QuoteDeatils.vue uses the Composition API to fetch a specific quote when the component is mounted, and provides methods to delete the current quote or update one quote, redirecting to the home page after either operation.

EditQuote.vue

EditQuote.vue
<template>
  <div class="container mx-auto p-4">
    <h1 class="text-4xl font-semibold mb-4">Edit quote</h1>
    <form @submit.prevent="updateQuote" class="bg-white rounded-lg shadow-md p-6">
      <div class="mb-4">
        <label for="content" class="block text-gray-700 font-medium">Quote:</label>
        <input
          v-model="content"
          type="text"
          id="content"
          class="block w-full mt-1 bg-gray-100 border border-gray-300 rounded p-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
        />
      </div>
      <div class="mb-4">
        <label for="author" class="block text-gray-700 font-medium">Author:</label>
        <input
          v-model="author"
          type="text"
          id="author"
          class="block w-full mt-1 bg-gray-100 border border-gray-300 rounded p-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
        />
      </div>
      <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
        Update
      </button>
    </form>
  </div>
</template>

  
  <script>
  import { useQuotesStore } from '../stores/quotes';
  
  export default {
    data() {
      return {
        content: '',
        author: '',
      };
    },
    async created() {
      const quotesStore = useQuotesStore();
      await quotesStore.fetchQuote(this.$route.params.id);
      this.content = quotesStore.currentQuote.content;
      this.author = quotesStore.currentQuote.author;
    },
    methods: {
  async updateQuote() {
    const quotesStore = useQuotesStore();
    const quote = {
      _id: this.$route.params.id, // Assuming the id is available in the route params
      content: this.content,
      author: this.author,
    };
    await quotesStore.updateQuote(quote);
    this.$router.push('/');
  },
},


    }
  </script>
  

EditQuotes.vue allows users to edit a quote. It fetches a specific quote on the creation and updates the quote’s content and author based on user input when the updateQuote method is called

Conclusion

We have come to the end of this MEVN CRUD tutorial, I hope you learned a few things from the tutorial. In this article we learned about creating a server with node and express, connecting to MongoDB, and performing CRUD operations. The code for this tutorial can be found here.