Setting up an express application with controllers and routing
Joseph-Peter Yoofi Brown-Pobee
Posted on July 25, 2022
Created: May 11, 2022 2:41 AM
Published: Yes
Set Up
We initialise our node project in our directory of choice, create a server directory and a server.js file for our node server
npm init -y
mkdir server
touch server/server.js
We install babel
and webpack
dev-dependencies to bundle and minify our code for eventual production
npm install -D @babel/core @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli webpack-node-externals
đź’ˇ webpack-node-externals
prevents us from having modules in our node_modules folder bundled into our code as this would make it bloated. Webpack
will load modules from the node_modules folder and bundle them in. This is fine for frontend code, but backend modules typically aren't prepared for this. Read more here
We then configure webpack
by creating a webpack.config.js
at the root of our project directory
const path = require('path');
const CURRENT_WORKING_DIR = process.cwd();
const nodeExternals = require('webpack-node-externals');
const config = {
name: "server",
entry: [ path.join(CURRENT_WORKING_DIR, '/server/server.js')],
target: "node",
output: {
path: path.join(CURRENT_WORKING_DIR, '/dist/'),
filename: "server.generated.js",
publicPath: '/dist/',
libraryTarget: "commonjs",
},
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader']
}
]
}
};
module.exports = config;
We configure babel by creating a .babelrc
file in our project root
{
"presets": [
["@babel/preset-env",
{
"targets": {
"node": "current"
}
}]
]
}
You can read more about code bundling with these resources:
What is bundling with webpack?
Demystifying Code Bundling with JavaScript Modules
Next we install express to begin building our server
npm install express
We will install the following packages as express middleware to give our server some enhanced functionality
body-parser: For parsing the incoming request body as json and urlencoded values (also works for FormData). The request body would then be available for our handlers.
cookie-parser: For parsing cookies in request headers and making them available in the request object for our handlers
helmet: For adding necessary security headers to our server to improve security. This is necessary for guarding against basic attacks like cross site scripting attacks. It is not the end enough on its own but is useful to provide some basic level of security
compression: For compressing response bodies
cors: For setting up our Cross Origin Resource Sharing policy to enable us clients from allowed domains to communicate with our server. A good write up on Cross Origin Requests can be viewed here
npm install body-parser cookie-parser helmet compression cors
We can then create our express app in an express.js
file in the server directory
import express from "express";
import bodyParser from "body-parser";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import compress from "compression";
import cors from "cors";
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cookieParser());
app.use(helmet());
app.use(compress());
app.use(cors());
export default app;
We export our express app to be imported in our main server.js
file. We a file config.js to hold necessary configurations to be used across our server and export a single config object to be imported in server.js
export default {
env: process.env.NODE_ENV || 'development',
port: process.env.PORT || 4000
}
import mongoose from "mongoose";
import config from './config/config'
app.listen(config.port, (err) => {
if (err) {
console.error(err);
} else {
console.info(`Application running on PORT: ${config.port}`)
}
})
With our server files set up we can now start our dev server. Nodemon is a useful resource of running your dev server as it automatically restarts (kind of like hot reload) when you make a change to your source code
npm install -D nodemon
We then create a nodemon
config file nodemon.json
and set it up to run webpack
and our generated code from the dist
directory we indicated in webpack.config.js
{
"verbose": false,
"watch": ["./server"],
"exec": "webpack --mode=development --config webpack.config.server.js && node ./dist/server.generated.js"
}
Finally we set up a dev script in our package.json to run nodemon with the above config to set up our run our dev server
{
"name": "social-media-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon", //. <-- HERE
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.2",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.17.3",
"helmet": "^5.0.2",
},
"devDependencies": {
"@babel/core": "^7.17.5",
"babel-loader": "^8.2.3",
"nodemon": "^2.0.15",
"webpack": "^5.69.1",
"webpack-cli": "^4.9.2",
"webpack-node-externals": "^3.0.0"
}
}
We can now run the dev script to start our server
npm run dev
The output below shows our code has been bundled by webpack
and the generated file is currently being run and the server is running.
Now we can begin the real work
Users
Users are the first resource we are going to work with. We want to have the ability to create, read, update and delete users as well as have the foundation for basic authentication.
We will use Prisma as the Object Relational Mapper for our MongoDB database. Mongoose is also another option but I like Prisma’s type safety when used with typescript and schema declaration as it makes maintenance easier.
The team at Prisma have a walkthrough for setting up Prisma with MongoDB and it can be found here:
Start from scratch with MongoDB (15 min)
We can follow throw the steps to begin set up till schema setup. Below is the schema for our User Model
type UsersImage {
contentType String
data Bytes
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v") @default(0)
createdAt DateTime @default(now()) @db.Date
email String @unique
password String
image UsersImage?
name String
salt String
updatedAt DateTime @updatedAt @db.Date
about String?
@@map("users")
}
We define a field name in the first column, field type in the next and field attributes and modifiers in the final column
- id: The primary key for each user document. This will be a string will be denoted as the primary key using the
@id
. The@default(auto())
modifier auto generates an ID by default when a document is created. @map enables us to set the field name within the database. In MongoDB the underlyingÂID
field name is alwaysÂ_id
and must be mapped withÂ@map("_id")
. When we create documents and view them in the database we will see_id
as a field and when we use the Prisma ORM in our development we would be able to access this asid
on the user object. Prisma will take care of the mapping. @db.ObjectId is a native database type attribute that allows us to specify the data type for the field. ObjectIds are field types native to MongoDB documents similar to how SQL databases have types such as VARCHAR. - v: This field on the document is for document versioning. In the future if we begin saving a different version of documents but do not want to discard the old data, this field would enable us to differentiate between the version of documents to use. You can read more about the Document Versioning Pattern here. We set its default version to 0
- createdAt and updatedAt: We store the dates when documents are created and updated
- email: We store users email and ensure it is unique
- password: Users hashed password to be stored
- image: For now we will be storing a users image data as Binary Data in the database. Note that this is not best practice and later on we will work at migrating the schema to store links to the image to be fetched from remote storage. We create a
type
field to define shape of our image. We will store the data and the type of content (it’s MIME type) - name: User’s name as string
- about: Short description of the user
- salt: This is a piece of random data used when we are encrypting the user’s plain text password during authentication. My having a different salt value for different users we increase our password security as it limits the effects of common passwords being exploited. Salting’s Wikipedia page is a great place to get up to speed on the benefits of using a salt.
A full reference of Prisma’s fields and modifiers can be found here
After defining our model we can proceed to install our Prisma client
npm install @prisma/client
We then run prisma generate
which reads our schema and generates a version of prisma client (with type defs and safety) tailored to our schema.
prisma generate
To enable use use prisma we need to initialise the prisma client which will allow us to query our database.
touch server/prisma/prisma.js
import {PrismaClient} from '@prisma/client'
const prisma = global.prisma || new PrismaClient()
if (process.env.NODE_ENV === 'development') {
if (!global.prisma) {
global.prisma = prisma
global.prisma.$connect().then(() => {
console.log('Database connected successfully')
});
}
} else {
prisma.$connect().then(() => {
console.log('Database connected successfully')
});
}
export default prisma;
We import that PrismaClient connect to the database and export the prisma object for use in our app. During development is common to restart the server many times as we make changes hence to avoid creating multiple connections we attach the prisma object to the global node object (known as globalThis
). We check if this object exists on the global object when the app starts or create a new client otherwise. Note the assignment to global is only done in development
With that done we can now begin querying the database.
We want to make it possible to create, read, update and delete a user in our application. We will do this in a user controller using the Prisma client API to query the database.
touch server/controllers/user.controller.js
Let’s define a method to create and store users in the database. This would be useful during the sign up process
import crypto from 'crypto';
import isEmail from 'validator/lib/isEmail';
import contains from 'validator/lib/contains';
import prisma from '../prisma/prisma';
import {ValidationError} from "../helpers/db.error.handler";
const encryptPassword = (password, salt) => {
try {
return crypto.createHmac('sha1', salt).update(password).digest('hex');
} catch (err) {
console.error(err);
}
}
const makeSalt = () => String(Math.round((new Date().valueOf() * Math.random())))
const emailIsValid = (email) => {
return isEmail(email)
}
const nameIsValid = (name) => {
return name.length > 1
}
const passwordIsValid = (password) => {
const errors = [];
if (!password) {
errors.push('Password is required')
}
if (password?.length < 8) {
errors.push('Password must be at least 8 characters')
}
if (password && contains(password, 'password')) {
errors.push('Password should not contain the word password')
}
if (errors.length > 0) {
return {
isValid: false,
message: errors.join(',')
}
} else {
return {
isValid: true
}
}
}
const store = async (req, res, next) => {
const {name, email, password} = req.body;
const userData = {name, email, password};
try {
const errors = {};
if (!nameIsValid(name)) {
errors.name = 'User name too short'
}
if (!emailIsValid(email)) {
errors.email = 'Invalid email'
}
const existingUser = await prisma.findUnique({
where: {
email
}
})
if (existingUser !== null) {
throw new ValidationError({
error: "User already exists"
})
}
const passwordValid = passwordIsValid(password)
if (!passwordValid.isValid) {
errors.password = passwordValid.message
}
if (Object.keys(errors).length > 0) {
throw new ValidationError(errors);
}
const salt = makeSalt();
const hashed_password = encryptPassword(password, salt);
await prisma.create({
data: {
name,
email,
password: hashed_password,
salt,
}
})
return res.status(200).json({
message: 'User saved successfully'
})
} catch (e) {
if (e instanceof ValidationError) {
return res.status(422).json(e.errors)
}
next(e);
}
};
export default { store }
Let’s break the above down. We have created a method store
, which is Express middleware, to handle the creation of users when a request is sent to this route. It takes, the incoming request object as a parameter as well as an object for dispatching a response. The next
function is useful if we intend on passing an error to an error handler or relinquishing control to the next middleware. In this case we will be using next to send any unexpected errors to our error handler. You can learn all about how express works from its documentation
We destructure the required keys from the request body and begin validation. We create separate functions to handle validation of our data. We create an error object that will be used to hold the validation errors if the request data does not pass our validation.
nameIsValid
checks that there is more than a single character for the users name
emailIsValid
uses the helper method isEmail
from the validator package to assert that the provided email is a valid email format
We also check if the user already exists and throw and error immediately if this is the case
passwordIsValid
checks that the password is present, is at least 8 characters and does not contain the word “password”. We store the individual error messages in an array and concatenate and return this as the message property of the output object as well as isValid
to denote whether the password is valid
If any of these validation checks are failed, the field names are added to the error object. Based on the number of keys on the error object after validation we throw a ValidationError with the errors as part of the constructor. We create the ValidationError as shown below
touch server/helpers/db.error.handler.js
export class ValidationError extends Error {
constructor(errors) {
super();
this.errors = errors;
}
}
We check for this error in the catch
block of our try catch
and based on that send a 422 status code and an object of errors as a response
After all the data is validated, we create a salt using makeSalt
which simply generates a random set of numbers based on the current date in milliseconds. We use this to encrypt the users password using node’s crypto
package.
Finally we use the prisma client to create the user and send a 200 response with a message back to the user. If an error occurs that is not a ValidationError we forward this to our error handler user next(e)
We will cover this error handler soon. Finally we export the store method as part of an object default export. This will allow other modules to have access to it.
Routing
Now that we have our controller that would handle the creation of users we need to make it possible to receive data to create users. We do this by hooking up our controllers to routes for our express server to use.
import express from 'express';
import userController from '../controllers/user.controller';
const userRouter = express.Router();
userRouter.route('/api/users')
.post(userController.store)
export default userRouter
Above we:
- Import the
express
module and the exporteduserController
object - Create an
express
router to handle routing - Route
POST
requests to/api/users
and handle them with thestore
method we created - Export the
userRouter
Our final step is to “attach” our userRouter to our server
import express from "express";
import bodyParser from "body-parser";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import compress from "compression";
import cors from "cors";
import userRouter from "./routes/user.routes"; //new
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cookieParser());
app.use(helmet());
app.use(compress());
app.use(cors());
app.use(userRouter); //new
export default app;
With our router hooked up to our server we can spin up our server to begin receiving requests
npm run dev
Using post man we make a POST
request to our server (running on port 4000 remember) at the route /api/users
to create a user
We get a successful response from the server
We can check our database to see the user created successfully
We have successfully set up a simple express application, created a controller to handle logic and set up routes to direct requests to our controllers. On our controller we put all the methods directly into the handler but its useful to encapsulate the logic in a service or helper function that insulates the handler from changes that are at a lower level of abstraction. That way we can simple call a service to create a user and all the logic would be contained there thereby ensuring our controller is clean and operates at an appropriate level of abstraction,
Listing, deleting and updating are all further functionalities we can incorporate into our server by creating more services, controllers and routes as well as sending the appropriate queries to our Mongo database using our Prisma Client. The Prisma docs are full of details on how to do these and you can check these out to expand the set up and create a simple CRUD API
Posted on July 25, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.