Stafini
BlogFlashcardsProjectsResume

Express.js best practices for beginner

When I initially planned to create this blog website, I didn't envision the setup that I have now, but that's a topic for another post. I had separated the frontend and backend as distinct entities, with the frontend making API calls to the backend to fetch post data. It also presented an excellent opportunity to refresh my knowledge of backend development, which had remained mostly theoretical until this project.

The code

Backend code

The functionalities for a CRUD app are fairly simple, and I used the following packages and libraries:

  • Typescript
  • Express.js
  • Nodemon
  • JWT Token
  • TypeORM
  • PostgreSQL

Unified Routing

To start, connect your app with use() in your entry script file:

app.use('/api/v1', routes)

Create a folder named routes to store all route configurations, and add an index.ts file. Each file is dedicated to one API route following RESTful conventions.

|-- routes
    |-- index.ts
    |-- auth.route.ts
    |-- posts.route.ts
    |-- tag.route.ts

Add route methods in the route files, such as for the signup and login routes. More on the Controller shortly.

import express from 'express'

const route = express.Router()

route.route('/auth/signup').post(AuthController.signup)
route.route('/auth/login').post(AuthController.login)

export default route

That's it! This method offers a clean and concise way to define routes and makes it easy to expand.

Handling Server Errors

Have you ever encountered situations like server errors, records not found, or validation errors? This is how I used to handle them when I first learned Express.js:

try {
	// perform some fetching or signing logic
} catch (error) {
	// throw an error
	if (error instanceof Error) {
		throw new Error(error.message)
	}
}

While this approach is correct, I found it repetitive. Here's a more elegant solution:

First, create a middleware function to return a Promise.catch on error. Create a folder named middlewares and place the asyncHandler.ts file in it.

// asyncHandler.ts
import { NextFunction, Response, Request } from 'express'

export const asyncHandler =
  (fn: (req: Request, res: Response, next: NextFunction) => void) =>
  (req: Request, res: Response, next: NextFunction) => {
    return Promise.resolve(fn(req, res, next)).catch(next)
  }

Then, create another folder named exceptions and add a customError.ts class. This class serves as the base for creating custom errors based on HTTP codes, such as 400, 401, 403, and 500.

export class CustomError extends Error {
  message!: string
  status!: number
  additionalInfo!: any | any[]

  constructor(message: string, status = 500, additionalInfo: any | any[] = undefined) {
    super(message)
    this.message = message
    this.status = status
    this.additionalInfo = additionalInfo
  }
}

export interface IResponseError {
  message: string
  additionalInfo?: string
}

Make an error class extended from our customError named clientErorr.ts, Let's do it for 400 error code:

import { CustomError } from './customError'

export class ClientError extends CustomError {
  constructor(message: string) {
    super(message, 400)
  }
}

Now, create an error class extended from customError named clientError.ts, which is tailored for 400 error codes:

// post.route.ts
route.route('/posts/:id').post(asyncHandler(PostController.getById))

// posts.controller.ts
// No need to throw errors anymore.
const post = await postRepository.findOne({
    where: {
        slug,
    },
    relations: {
        tags: true,
    },
})
// Customize error messages as well
if (!post) throw new NotfoundError('Post does not exist!!!')
return res.json(formatResponse(post))

This is the response you'll get in Postman if the post is not found:

{
	message: 'Post does not existed !!!'
}

Unified Controller Logic

I find this practice simple but effective in making route logic easy to follow. The idea is to create one controller for each resource, such as auth.controller.ts for auth.route.ts.

We can declare the controller like this:

export class AuthController {
    static login = async (req: Request, res: Response) => {
        // sign-in logic
    }
}

Using JWT for Route Authentication Middleware

Install jsonwebtoken in case you have not:

yarn add jsonwebtoken

or

npm install jsonwebtoken

Create a middle file name checkJwt.ts in the middlewares folder:

import { Request, Response, NextFunction } from 'express'
import { JwtPayload, verify } from 'jsonwebtoken'

interface CustomRequest extends Request {
  token: JwtPayload
}

const jwtConfig = {
  secret: 'JWT_SECRET',
  audience: 'JWT_AUDIENCE',
  issuer: 'JWT_ISSUER',
  expiredIn: '1y',
  refreshExpiredIn: '1y',
}


export const checkJwt = (req: Request, res: Response, next: NextFunction) => {
  const token = <string>req.headers['authorization']
  let jwtPayload

  try {
    jwtPayload = verify(token.split(' ')[1], jwtConfig.secret!, {
      complete: true,
    })
    ;(req as CustomRequest).token = jwtPayload
  } catch (error) {
    res
      .status(401)
      .type('json')
      .send(JSON.stringify({ message: 'Missing or invalid token' }))
    return
  }
  next()
}

This is a basic setup where we verify the token with our secret. If it passes, we call next() to continue the flow to the next middleware; otherwise, we return a 401 error.

You can now authenticate your routes by using checkJwt as a middleware function:

import {checkJwt} from './checkJwt'

router.route('/posts').post([checkJwt], asyncHandler(PostController.createPost))

Request Validation

Typically, you can check request data in your controller logic like this:

static login = async (req: Request, res: Response) => {
    const { username, password } = req.body
    if (!username || !password) throw new ClientError("Username and password are required!!!")
}

This approach is fine for simple and minimal field checking, but for more extensive checks, it can make your controller logic challenging to maintain. You can move the validation logic into a middleware. First, install express-validator:

yarn add express-validator

or

npm install express-validator

Create another file named validator.ts in middlewares folder

import { NextFunction } from 'express'
import { Request, Response } from 'express-serve-static-core'
import { body, validationResult } from 'express-validator'
import { CustomError } from './customError'

export const authValidation = () => {
    return [
        body('username').notEmpty().withMessage('Username is required'),
        body('password').isLength({ min: 5 }).withMessage('Password must be more than 5 characters'),
    ]
}

export const validate = (req: Request, res: Response, next: NextFunction) => {
    const errors = validationResult(req)

    if (errors.isEmpty()) return next()

    const extractedErrors: { [x: string]: any }[] = []

    errors.array().map((err) => {
        if (err.type === 'field') {
            extractedErrors.push({ [err.path]: err.msg })
        }
    })
    // Utilize our CustomError class
    throw new CustomError('Request error', undefined, extractedErrors)
}

Use it as a middleware in your routing configuration:

import { authValidation, validate } from './middlewares/validator'

route.route('/auth/login').post(authValidation(), validate, asyncHandler(AuthController.login))

This setup is straightforward and keeps your route and controller logic clean and focused.


That's it, folks! These are the lessons I've learned during six weeks of working on this project. While the backend server may not have made it into production, I consider these codes valuable for my learning experience and for getting back into backend coding.