Introduction
Building a web application with Node.js allows for the creation of scalable and efficient platforms. This article will guide you through setting up a simple application with user authentication and a dashboard interface. We'll use MongoDB for the database, Express for the web framework, and EJS for templating. Additionally, we will containerize our application with Docker for easy deployment.
Prerequisites
Before you begin, ensure you have the following installed on your system:
- Node.js and npm
- Docker
- Docker Compose
- A text editor or IDE of your choice
Step 1. Setting Up Your Project
-
Initialize a New Node.js Project
-
Install Dependencies
-
Install Express, EJS, Mongoose, and other necessary packages:
npm install express ejs mongoose bcryptjs express-session passport passport-local
Step 2. Create the Application Structure
Create the necessary directories and files for your application:
nodejs-app/
│
├── app.js
├── Dockerfile
├── docker-compose.yml
├── package.json
├── routes/
│ ├── index.js
│ ├── user.js
├── config/
│ ├── passportConfig.js
├── views/
│ ├── login.ejs
│ ├── register.ejs
│ ├── dashboard.ejs
│ ├── layout.ejs
│ └── partials/
│ └── messages.ejs
├── models/
│ ├── User.js
└── public/
├── css/
├── style.css
Here is a shell script (create-nodejs-app.sh
) to automatically create the directory structure for your Node.js application:
#!/bin/bash
# Define the folder structure
mkdir -p nodejs-app/{routes,config,views,models,public/css}
# Create the necessary files
touch nodejs-app/app.js
touch nodejs-app/Dockerfile
touch nodejs-app/docker-compose.yml
touch nodejs-app/package.json
touch nodejs-app/routes/index.js
touch nodejs-app/routes/user.js
touch nodejs-app/config/passportConfig.js
touch nodejs-app/views/login.ejs
touch nodejs-app/views/register.ejs
touch nodejs-app/views/dashboard.ejs
touch nodejs-app/views/layout.ejs
touch nodejs-app/views/partials/messages.ejs
touch nodejs-app/models/User.js
touch nodejs-app/public/css/style.css
# Confirmation message
echo "Node.js app folder structure created successfully!"
Step 3. Developing the Backend
-
Set Up Express and Middleware
- In
app.js
, Set up your Express application and middleware to handle requests.
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const flash = require('connect-flash');
const passport = require('passport');
const app = express();
const port = 3000;
// Passport Config
require('./config/passportConfig')(passport);
// DB Config
mongoose.connect('mongodb://mongo:27017/nodejs-app', { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('MongoDB Connected'))
.catch(err => console.log(err));
// Middleware
app.use(express.urlencoded({ extended: false }));
app.use(express.static('public'));
// EJS
app.set('view engine', 'ejs');
// Express Session
app.use(session({
secret: 'secret',
resave: true,
saveUninitialized: true
}));
// Passport middleware
app.use(passport.initialize());
app.use(passport.session());
// Connect Flash
app.use(flash());
// Global Variables
app.use((req, res, next) => {
res.locals.success_msg = req.flash('success_msg');
res.locals.error_msg = req.flash('error_msg');
res.locals.error = req.flash('error');
next();
});
// Routes
app.use('/', require('./routes/index'));
app.use('/users', require('./routes/user'));
// Start Server
app.listen(port, () => {
console.log(`Server started on port ${port}`);
});
- Configure Passport for user authentication.
-
Database Models
- Use Mongoose to define models in
models/User.js
.
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});
const User = mongoose.model('User', UserSchema);
module.exports = User;
-
User Authentication
- Implement registration and login functionality in
routes/user.js
.
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const passport = require('passport');
const User = require('../models/User');
// Register Page
router.get('/register', (req, res) => res.render('register'));
// Register Handle
router.post('/register', (req, res) => {
const { name, email, password, password2 } = req.body;
let errors = [];
// Validation
if (!name || !email || !password || !password2) {
errors.push({ msg: 'Please fill in all fields' });
}
if (password !== password2) {
errors.push({ msg: 'Passwords do not match' });
}
if (errors.length > 0) {
res.render('register', { errors, name, email, password, password2 });
} else {
User.findOne({ email: email })
.then(user => {
if (user) {
errors.push({ msg: 'Email already exists' });
res.render('register', { errors, name, email, password, password2 });
} else {
const newUser = new User({ name, email, password });
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (err, hash) => {
if (err) throw err;
newUser.password = hash;
newUser.save()
.then(user => {
req.flash('success_msg', 'You are now registered and can log in');
res.redirect('/users/login');
})
.catch(err => console.log(err));
});
});
}
});
}
});
// Login Page
router.get('/login', (req, res) => res.render('login'));
// Login Handle
router.post('/login', (req, res, next) => {
passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/users/login',
failureFlash: true
})(req, res, next);
});
// Logout Handle
router.get('/logout', (req, res) => {
req.logout(() => {
req.flash('success_msg', 'You are logged out');
res.redirect('/users/login');
});
});
module.exports = router;
- Utilize Passport for handling authentication. config/passportConfig.js
const LocalStrategy = require('passport-local').Strategy;
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
// Load User model
const User = require('../models/User');
module.exports = function (passport) {
passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
User.findOne({ email: email })
.then(user => {
if (!user) {
return done(null, false, { message: 'That email is not registered' });
}
bcrypt.compare(password, user.password, (err, isMatch) => {
if (err) throw err;
if (isMatch) {
return done(null, user);
} else {
return done(null, false, { message: 'Password incorrect' });
}
});
})
.catch(err => console.log(err));
}));
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user);
});
});
};
routes/user.js
.
const express = require('express');
const router = express.Router();
const { ensureAuthenticated } = require('../config/auth');
// Welcome Page
router.get('/', (req, res) => res.render('welcome'));
// Dashboard Page
router.get('/dashboard', ensureAuthenticated, (req, res) =>
res.render('dashboard', {
user: req.user
})
);
module.exports = router;
- config/auth.js
module.exports = {
ensureAuthenticated: function (req, res, next) {
if (req.isAuthenticated()) {
return next();
}
req.flash('error_msg', 'Please log in to view this resource');
res.redirect('/users/login');
}
};
Step 4. Creating the Frontend
-
EJS Templates
- Create views for registration, login, and the dashboard in the
views/
directory.
- views/register.ejs
<%- include('layout') %>
<form action="/users/register" method="POST">
<div class="form-group">
<input type="text" name="name" class="form-control" placeholder="Name">
</div>
<div class="form-group">
<input type="email" name="email" class="form-control" placeholder="Email">
</div>
<div class="form-group">
<input type="password" name="password" class="form-control" placeholder="Password">
</div>
<div class="form-group">
<input type="password" name="password2" class="form-control" placeholder="Confirm Password">
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
- views/login.ejs.
<%- include('layout') %>
<form action="/users/login" method="POST">
<div class="form-group">
<input type="email" name="email" class="form-control" placeholder="Email">
</div>
<div class="form-group">
<input type="password" name="password" class="form-control" placeholder="Password">
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
- views/dashboard.ejs
<%- include('layout') %>
<h1>Welcome, <%= user.name %>!</h1>
<a href="/users/logout" class="btn btn-danger">Logout</a>
- views/layout.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Node.js App</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/users/login">Login</a>
<a href="/users/register">Register</a>
</nav>
<div class="container">
<%- include('partials/messages') %>
</div>
</body>
</html>
-
Create the partials
Directory and messages.ejs
- Inside your
views
directory, create a new folder called partials
.
- Inside this
partials
folder, create a file called messages.ejs
.
-
Here is an example of the content for messages.ejs
:
<% if (typeof success_msg != 'undefined') { %>
<div class="alert alert-success">
<%= success_msg %>
</div>
<% } %>
<% if (typeof error_msg != 'undefined') { %>
<div class="alert alert-danger">
<%= error_msg %>
</div>
<% } %>
<% if (typeof errors != 'undefined' && errors.length > 0) { %>
<div class="alert alert-danger">
<ul>
<% errors.forEach(function(error) { %>
<li><%= error.msg %></li>
<% }) %>
</ul>
</div>
<% } %>
-
Static Files
- Style your application using CSS in the
public/css/style.css
file.
/* Reset some default browser styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333;
line-height: 1.6;
margin: 0;
padding: 0;
}
/* Container */
.container {
max-width: 1170px;
margin: 0 auto;
padding: 20px;
}
/* Navigation */
nav {
background: #333;
color: #fff;
padding: 15px;
text-align: center;
}
nav a {
color: #fff;
text-decoration: none;
margin: 0 10px;
}
nav a:hover {
text-decoration: underline;
}
/* Form Styling */
form {
background: #fff;
padding: 20px;
margin-top: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 15px;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
button {
background: #333;
color: #fff;
border: 0;
padding: 10px 15px;
cursor: pointer;
border-radius: 5px;
}
button:hover {
background: #555;
}
/* Messages */
.alert {
padding: 10px;
background-color: #f4f4f4;
color: #333;
margin-bottom: 20px;
border: 1px solid #ccc;
}
.alert-success {
background-color: #dff0d8;
color: #3c763d;
}
.alert-error {
background-color: #f2dede;
color: #a94442;
}
/* Dashboard */
h1 {
font-size: 24px;
margin-bottom: 20px;
}
.btn-danger {
background-color: #e74c3c;
}
.btn-danger:hover {
background-color: #c0392b;
}
Step 5. Dockerizing the Application
-
Dockerfile
-
Docker Compose
Step 6. Running Your Application