MVC Architecture with Express Server

Lauren Cunningham
Geek Culture
Published in
5 min readMay 27, 2021

Structuring MERN Application Back-end

photo credit: critical technology

MVC Overview

When beginning a new project, there are several different methods that can be used to set up the overall file structure and flow. One of the most commonly used architectural patterns is called MVC. This is an acronym for Model, View, Controller.

This pattern is favored because of its alignment with the computer science design principle, Separation of Concerns. By dividing up responsibilities within our file structure, we can encapsulate information to be referred to via abstraction and maintain cleaner codebases.

The MVC structure separates concerns by making the Model responsible for data being used by our application while the Controller handles the business logic needed to receive HTTP requests and manipulate our data if necessary before it is presented by the View which contains the UI implementation.

The file structure for your application should look something like this…

app-name-here
|- controllers
|- db
|- models
|- node_modules
|- views
.gitignore
index.js
package.json
package-lock.json

Mongoose Setup (Models)

Although we could use MongoDB alone, Mongoose gives us an additional layer of methods to use on objects created in the database. To use Mongoose we need to install by running:

npm i mongoose

Then we can connect to the database by creating a connection.js file in db folder. Be sure to require mongoose at the top of the file.

const mongoose = require('mongoose')

Then define the database URI. This should point to a secret variable in a .env file and be dynamically assigned based on whether you running in production or development.

const URI = 
process.env.NODE_ENV === 'production'
//if in production use this URI
? process.env.DB_URL
//otherwise use this
:'mongodb://localhost/app_name_here';

Lastly, you want to use the mongoose connect method. This takes two arguments, the database URI and an object of optional configuration.

mongoose.connect(URI, {
useNewUrlParser: true,
useCreateIndex: true,
//continue with any other options here
})

It’s helpful to add .then() and .catch() functions after this connect method to confirm it’s working properly. Don’t forget to export mongoose at the bottom of the file to be able to import it wherever it’s needed.

module.exports = mongoose

Setting up Schema and Model

In the model folder, create a file with the following convention: model-name-here.js.

This file will contain our schema definition and expected fields for the model. Here’s an example.

const mongoose = require('mongoose')const userSchema = new mongoose.Schema ({ name: {  type: String,  required: true,  trim: true}, email: {  type: String,  required: true,  unique: true,  lowercase: true, }, password: {   type: String,   required: true,   trim: true  },  {timestamps: true})const User = mongoose.model('User', UserSchema);

module.exports = User;

Now we can load up some test data and check it out. You’ll need to create a file to hold an array of JSON objects containing all the fields your schema requires and another file with methods to delete any existing data and insert seed data from the .json file. These files will live in the db folder as well.

//userSeeds.json[ {  "name": "John",  "email": "john@example.com",  "password": "123ffedead" }, {  "name": "Joey",  "email": "joey@example.com",  "password": "123fffeaedead" }]//userSeeds.jsconst User = require('./models/Users')const seedData = require('./userSeeds.json') 
User.deleteMany({}) .then(() => { return User.insertMany(seedData) }) .then(console.log(seedData)) .catch(console.error) .finally(() => { process.exit() })

Run node ./db/userSeeds.js to load the data. You should see the seeded data in your terminal, but you can also run db.users.find() in the mongo shell to confirm it was inserted.

Create Server (and Controllers)

To get everything up and running, let’s talk about how to create an express server and then add some controllers that will define some paths and methods needed for our UI.

The index.js file at the top directory is where we’ll set all of this up. Of course we need to require express first. After that we can create a variable for our model and the port we’d like to run on. Finally, we need to use the .listen method on the express server with the port as the first argument and a callback function that indicates it’s running as the second argument.

//index.js

const express = require('express');
const app = express();

// Require User model
const User = require('./models/user.js');

// Define a port variable
const port = process.env.PORT || 5000;

//Run the server
app.listen(port, () => {
console.log(`App running on port ${port}`);
});

At this point, running npm run index.js should get the server started and you should be able to see the confirmation message from the .listen method in your terminal.

Now we need to include a controller defines what data should be returned at a given endpoint. Following the MVC structure, we will add this logic to a file in the controllers folder. We’ll want to require express as well as the model file and add a router variable that runs express.Router.

In order to get all user objects we can set up a controller that points to the todos/index and gives us all users. In the sample code below, you’ll see that the .find method has an empty object as the argument. Not defining a specific identifier allows all users to be returned.

//controllers/users.jsconst express = require('express');
const router = express.Router();
const User = require('../models/user.js')router.get('/users', (req, res) => {
User.find({})
.then((users) => {
res.render('users/index', {users});
})
.catch(console.error)
})

Since we have a separate file for our user controller now, we need to adjust our index.js file. We can replace the user model requirement with a user controller requirement and add a method to tell the app to use the controller when at the index path.

//index.js

const express = require('express');
const app = express();

// Require User controller
const userController = require('./controllers/user.js');

// Define a port variable
const port = process.env.PORT || 5000;

//Run the server
app.listen(port, () => {
console.log(`App running on port ${port}`);
});
//Use the User controller at the /users path
app.use('/users', userController)

Prepare UI (Views)

Almost done! The last step is to define the view files we need in the views folder. These files need some HTML for the data to be displayed in the browser.

We can use a library called Handlebars to iterate over objects within the HTML code. Run npm i hbs to install this dependency and add the following line to your index.js file to make it available throughout your application.

app.set('view engine', 'hbs')

We indicate the beginning of an iteration with #each and end with /each. All files that utilize Handlebars need to have .hbs at the end of the file name.

Sticking with the example above we can create a subfolder for all user views called users and make a new file within that folder called index.hbs. The code within that file might look something like this…

//views/users/index.hbs
<h1>All Users</h1>
<ul>
{{#each users}}
<li>
<div>{{this.name}}</div>
</li>
{{/each}}
</ul>

Using RESTful route convention, you may also set up show, new, create, edit, update and/or destroy actions with their own views as well.

Recap

If you’re still with me, great work! We covered a lot here from reviewing the MVC architecture to getting models, controllers and views set and running on an express server. I hope this was valuable and you’re able to incorporate this into future projects. As always, I encourage you to continue to dive into the documentation. Look into MongoDB, Mongoose and Express guides for a better understanding of these concepts and additional examples. Until next time, happy coding!

--

--