DEV Community

Joseph-Peter Yoofi Brown-Pobee
Joseph-Peter Yoofi Brown-Pobee

Posted on

Setting up an express application with controllers and routing

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

đź’ˇ 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;
Enter fullscreen mode Exit fullscreen mode

We configure babel by creating a .babelrc file in our project root

{
  "presets": [
    ["@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }]
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
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}`)
    }
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

We can now run the dev script to start our server

npm run dev
Enter fullscreen mode Exit fullscreen mode

The output below shows our code has been bundled by webpack and the generated file is currently being run and the server is running.

Screenshot 2022-05-11 at 1.39.03 AM.png

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")
}
Enter fullscreen mode Exit fullscreen mode

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 as id 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
export class ValidationError extends Error {
    constructor(errors) {
        super();
        this.errors = errors;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Above we:

  1. Import the express module and the exported userController object
  2. Create an express router to handle routing
  3. Route POST requests to /api/users and handle them with the store method we created
  4. 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;
Enter fullscreen mode Exit fullscreen mode

With our router hooked up to our server we can spin up our server to begin receiving requests

npm run dev
Enter fullscreen mode Exit fullscreen mode

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

Screenshot 2022-06-02 at 12.05.23 AM.png

We get a successful response from the server

Screenshot 2022-06-02 at 12.15.47 AM.png

We can check our database to see the user created successfully

Screenshot 2022-06-02 at 12.18.38 AM.png

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

Top comments (0)