Express/EJS/MongoDB – Zero to Deploy tutorial

Repo With Code From this tutorial for reference

This article assumes basic knowledge of ExpressJS, if your new to Express or Mongo I recommend starting with the following Video Playlists:

Mongo Setup

  • go to mongodb.com and create an account
  • create a new free cluster (all the defaults are fine)
  • create username and password for accessing database (under database access)
  • whitelist all IP addresses under network access (0.0.0.0)
  • on the main dashboard, click on connect, select connecting your app and get the template url for connecting to your database.

mongo+srv://username:[email protected]/databaseName

make sure the username and password sections have the username and password you created under database access and the databaseName part can be anything you like.

This is your Mongo URI.

Express Setup

Setup

  • Open your IDE and terminal to an empty folder and type following commands

  • create a server.js touch server.js

  • create a new npm project npm init -y

  • install dependencies npm install express mongoose method-override ejs dotenv morgan

  • install nodemon globally npm install -g nodemon

  • setup the following scripts in package.json

"scripts"

:

{

"start"

:

"node server.js"

,

"dev"

:

"nodemon server.js"

}

,

Enter fullscreen mode

Exit fullscreen mode

Summary of Dependencies

- express => web framework for create server and writing routes

- mongoose => ODM for connecting to and sending queries to a mongo database

- method-override => allows us to swap the method of a request based on a URL query

- ejs => our templating engine

- dotenv => will allow us to use a `.env` file to define environmental variables we can access via the `process.env` object

- morgan => logs details about requests to our server, mainly to help us debug

Enter fullscreen mode

Exit fullscreen mode

  • create a .env file with the following dependencies
DATABASE_URL=<use your mongodb.com url>
PORT=4000

Enter fullscreen mode

Exit fullscreen mode

  • create a .gitignore file with the following (always a good habit to make one even if you have a global .gitignore, the global is there to catch you in case)
/node_modules
.env

Enter fullscreen mode

Exit fullscreen mode

Setting Up Our server.js

Import our dependencies

/////////////////////////////////////////////

// Import Our Dependencies

/////////////////////////////////////////////

require

(

"

dotenv

"

).

config

()

// Load ENV Variables

const

express

=

require

(

"

express

"

)

// import express

const

morgan

=

require

(

"

morgan

"

)

//import morgan

const

methodOverride

=

require

(

"

method-override

"

)

const

mongoose

=

require

(

"

mongoose

"

)

Enter fullscreen mode

Exit fullscreen mode

Establish Database Connection

/////////////////////////////////////////////

// Database Connection

/////////////////////////////////////////////

// Setup inputs for our connect function

const

DATABASE_URL

=

process

.

env

.

DATABASE_URL

const

CONFIG

=

{

useNewUrlParser

:

true

,

useUnifiedTopology

:

true

}

// Establish Connection

mongoose

.

connect

(

DATABASE_URL

,

CONFIG

)

// Events for when connection opens/disconnects/errors

mongoose

.

connection

.

on

(

"

open

"

,

()

=>

console

.

log

(

"

Connected to Mongoose

"

))

.

on

(

"

close

"

,

()

=>

console

.

log

(

"

Disconnected from Mongoose

"

))

.

on

(

"

error

"

,

(

error

)

=>

console

.

log

(

error

))

Enter fullscreen mode

Exit fullscreen mode

Create Our Todo Model

////////////////////////////////////////////////

// Our Models

////////////////////////////////////////////////

// pull schema and model from mongoose

const

{

Schema

,

model

}

=

mongoose

// make fruits schema

const

todoSchema

=

new

Schema

({

text

:

String

})

// make fruit model

const

Todo

=

model

(

"

Todo

"

,

todoSchema

)

Enter fullscreen mode

Exit fullscreen mode

Create App Object

/////////////////////////////////////////////////

// Create our Express Application Object

/////////////////////////////////////////////////

const

app

=

express

()

Enter fullscreen mode

Exit fullscreen mode

Register our Middleware

/////////////////////////////////////////////////////

// Middleware

/////////////////////////////////////////////////////

app

.

use

(

morgan

(

"

tiny

"

))

//logging

app

.

use

(

methodOverride

(

"

_method

"

))

// override for put and delete requests from forms

app

.

use

(

express

.

urlencoded

({

extended

:

true

}))

// parse urlencoded request bodies

app

.

use

(

"

/static

"

,

express

.

static

(

"

static

"

))

// serve files from public statically

Enter fullscreen mode

Exit fullscreen mode

Our initial route

////////////////////////////////////////////

// Routes

////////////////////////////////////////////

app

.

get

(

"

/

"

,

(

req

,

res

)

=>

{

res

.

render

(

"

index.ejs

"

,

{

greeting

:

"

Hello

"

})

})

Enter fullscreen mode

Exit fullscreen mode

Server Listener

//////////////////////////////////////////////

// Server Listener

//////////////////////////////////////////////

const

PORT

=

process

.

env

.

PORT

app

.

listen

(

PORT

,

()

=>

console

.

log

(

`Now Listening on port

${

PORT

}

`

))

Enter fullscreen mode

Exit fullscreen mode

The complete server.js file

/////////////////////////////////////////////

// Import Our Dependencies

/////////////////////////////////////////////

require

(

"

dotenv

"

).

config

()

// Load ENV Variables

const

express

=

require

(

"

express

"

)

// import express

const

morgan

=

require

(

"

morgan

"

)

//import morgan

const

methodOverride

=

require

(

"

method-override

"

)

const

mongoose

=

require

(

"

mongoose

"

)

/////////////////////////////////////////////

// Database Connection

/////////////////////////////////////////////

// Setup inputs for our connect function

const

DATABASE_URL

=

process

.

env

.

DATABASE_URL

const

CONFIG

=

{

useNewUrlParser

:

true

,

useUnifiedTopology

:

true

}

// Establish Connection

mongoose

.

connect

(

DATABASE_URL

,

CONFIG

)

// Events for when connection opens/disconnects/errors

mongoose

.

connection

.

on

(

"

open

"

,

()

=>

console

.

log

(

"

Connected to Mongoose

"

))

.

on

(

"

close

"

,

()

=>

console

.

log

(

"

Disconnected from Mongoose

"

))

.

on

(

"

error

"

,

(

error

)

=>

console

.

log

(

error

))

////////////////////////////////////////////////

// Our Models

////////////////////////////////////////////////

// pull schema and model from mongoose

const

{

Schema

,

model

}

=

mongoose

// make fruits schema

const

todoSchema

=

new

Schema

({

text

:

String

})

// make fruit model

const

Todo

=

model

(

"

Todo

"

,

todoSchema

)

/////////////////////////////////////////////////

// Create our Express Application Object

/////////////////////////////////////////////////

const

app

=

express

()

/////////////////////////////////////////////////////

// Middleware

/////////////////////////////////////////////////////

app

.

use

(

morgan

(

"

tiny

"

))

//logging

app

.

use

(

methodOverride

(

"

_method

"

))

// override for put and delete requests from forms

app

.

use

(

express

.

urlencoded

({

extended

:

true

}))

// parse urlencoded request bodies

app

.

use

(

"

/static

"

,

express

.

static

(

"

static

"

))

// serve files from public statically

////////////////////////////////////////////

// Routes

////////////////////////////////////////////

app

.

get

(

"

/

"

,

(

req

,

res

)

=>

{

res

.

render

(

"

index.ejs

"

,

{

greeting

:

"

Hello

"

})

})

//////////////////////////////////////////////

// Server Listener

//////////////////////////////////////////////

const

PORT

=

process

.

env

.

PORT

app

.

listen

(

PORT

,

()

=>

console

.

log

(

`Now Listening on port

${

PORT

}

`

))

Enter fullscreen mode

Exit fullscreen mode

  • create a views and static folder mkdir views static
  • create index.ejs in the views folder with the following

<!DOCTYPE html>

<html

lang=

"en"

>

<head>

<meta

charset=

"UTF-8"

>

<meta

http-equiv=

"X-UA-Compatible"

content=

"IE=edge"

>

<meta

name=

"viewport"

content=

"width=device-width, initial-scale=1.0"

>

<title>

Our Basic Todo App

</title>

</head>

<body>

<

%=

greeting

%

>

</body>

</html>

Enter fullscreen mode

Exit fullscreen mode

  • run server npm run dev
  • visit localhost:4000 to see if our test route works

Seeding some todos

Let’s seed our database with some initial todos using a seed route, a route whose only purpose is to reset our database with some sample data. This route should be commented out in production as you don’t want users erasing your database by accident. We will also update our main route so all the todos are being passed to the main page.

////////////////////////////////////////////

// Routes

////////////////////////////////////////////

app

.

get

(

"

/

"

,

async

(

req

,

res

)

=>

{

// get todos

const

todos

=

await

Todo

.

find

({})

// render index.ejs

res

.

render

(

"

index.ejs

"

,

{

todos

})

})

app

.

get

(

"

/seed

"

,

async

(

req

,

res

)

=>

{

// delete all existing todos

await

Todo

.

remove

({})

// add sample todos

await

Todo

.

create

([{

text

:

"

Eat Breakfast

"

},

{

text

:

"

Eat Lunch

"

},

{

text

:

"

Eat Dinner

"

}])

// redirect back to main page

res

.

redirect

(

"

/

"

)

})

Enter fullscreen mode

Exit fullscreen mode

Then update views/index.ejs to show all the todos:

<!DOCTYPE html>

<html

lang=

"en"

>

<head>

<meta

charset=

"UTF-8"

>

<meta

http-equiv=

"X-UA-Compatible"

content=

"IE=edge"

>

<meta

name=

"viewport"

content=

"width=device-width, initial-scale=1.0"

>

<title>

Our Basic Todo App

</title>

</head>

<body>

<h1>

Todos

</h1>

<ul>

<

%

for

(

todo

of

todos

)

{

%

>

<li><

%=

todo.text

%

></li>

<

%

}

%

>

</ul>

</body>

</html>

Enter fullscreen mode

Exit fullscreen mode

No go back to the main page, you will see no todos then to localhost:4000/seed and you’ll see the todos now show up since the seed route added them to the database.

Now let’s create a route so we can create todos and then we will add a form that posts to that route.

server.js

app

.

post

(

"

/todo

"

,

async

(

req

,

res

)

=>

{

//create the new todo

await

Todo

.

create

(

req

.

body

)

// redirect to main page

res

.

redirect

(

"

/

"

)

})

Enter fullscreen mode

Exit fullscreen mode

index.ejs

<

body

>

<

h1

>

Todos

<

/h1

>

<

h2

>

Add

Todo

<

/h2

>

<

form

action

=

"

/todo

"

method

=

"

post

"

>

<

input

type

=

"

text

"

name

=

"

text

"

placeholder

=

"

new todo

"

>

<

input

type

=

"

submit

"

value

=

"

create new todo

"

>

<

/form

>

<

ul

>

<%

for

(

todo

of

todos

)

{

%>

<

li

><%=

todo

.

text

%><

/li

>

<%

}

%>

<

/ul

>

<

/body

>

Enter fullscreen mode

Exit fullscreen mode

Refresh the main page, you should now see a form and when you fill it out and submit it will make a post request to our new route which will create the new todo then redirect us back to the main page!

Now let’s add the ability to remove todos. We will add a delete route that will delete the specified todo (the database id of the todo will be passed in the url as a param). After deleting the route will redirect us back to the main page. We will then add to our for loop in index.js a form that is just a submit button for making that delete request (we will use method override to overcome the method limitations of html forms.)

server.js

app

.

delete

(

"

/todo/:id

"

,

async

(

req

,

res

)

=>

{

// get the id from params

const

id

=

req

.

params

.

id

// delete the todo

await

Todo

.

findByIdAndDelete

(

id

)

// redirect to main page

res

.

redirect

(

"

/

"

)

})

Enter fullscreen mode

Exit fullscreen mode

index.ejs

<body>

<h1>

Todos

</h1>

<h2>

Add Todo

</h2>

<form

action=

"/todo"

method=

"post"

>

<input

type=

"text"

name=

"text"

placeholder=

"new todo"

>

<input

type=

"submit"

value=

"create new todo"

>

</form>

<ul>

<

%

for

(

todo

of

todos

)

{

%

>

<li><

%=

todo.text

%

>

<form

action=

"/todo/<%= todo._id %>?_method=delete"

method=

"post"

>

<input

type=

"submit"

value=

"delete todo"

>

</form>

</li>

<

%

}

%

>

</ul>

</body>

Enter fullscreen mode

Exit fullscreen mode

See that wasn’t so hard, right? Now let’s deploy it:

Deployment

  • commit and push the code up to github
  • create a new project on heroku.com
  • under the deployment tab, select the github method of deployment
  • select your repository from your github account
  • enable automatic deploys (so it’ll update when the repo updates)
  • click on manual deploy and watch it deploy

The app will still not be working yet cause it has no idea what your database string is since that was hidden in our .env file. To define environment variables on Heroku:

  • Go to the settings tab
  • scroll down and reveal the config vars
  • add a new variable with the key of “DATABASE_URL” and the key of your mongo uri (it has to be the same key you used in your local .env since)

That’s it, your app should be working now!

Keep On Learning

  • Add some CSS by adding a CSS file in the static folder and adding a link tag in the head of index.ejs

<link rel="stylesheet" href="/static/nameOfCssFile.css">

  • Similarly add a frontend JS file in your static file and connect it

<script src="/static/nameOfJsFile.js" defer></script>

  • You can also load other frontend libraries like jQuery, Alpine, HTMX, React and Vue with script tags, then you can use them to add more frontend interactivity

  • Use express routes to move the routes out of server.js into a controllers folder to better follow MVC architecture (you’ll need to know how to import and export in node)

  • Move the mongoose model code into a models folder for better MVC architecture

For small solo projects it’s ok to have everything in one file, but for group projects with lots of code you want the code broken up into many files with a common organization for better collaboration and less git merge conflicts (since people don’t have to work in the same file)