Skip to content

Bankole2000/service-reponse-formatter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Service Response Formatter

A Collection of utility functions to quickly scaffold and standardize server-side function returns and http responses with nodejs.


HTTP Icon

Service Response - YAHTTPRF

Yet Another HTTP Response Formatter

Table of Contents

Installation

Meant to be installed on a nodejs express server with either JavaScript or Typescript

npm install @neoncoder/service-response

The package exposes types, interfaces, and functions to help standardize and easily scaffold json http responses and function returns

ServiceResponse - interface for http responses

type ServiceResponse = {
  // data sent to the client (including pagination etc)
  data?: any, 
  // error summary (e.g. error.message)
  errMessage?: string | undefined | null, 
  // error details (raw error object / instance)
  error?: unknown, 
  // suggested fixes to help client / end user (e.g. contact support)
  fix?: string | undefined | null, 
  // short message describing operation result (e.g. 'Login successful')
  message: string, 
  // any req, res metadata (e.g. requestId, appId, version, timestamps etc)
  meta?: any, 
  // if using accesstoken and token is refreshed, you can send the new token here
  newAccessToken?: string | undefined | null, 
  // was the operation successful or not?
  success: boolean, 
  // server response status code - i.e. 200, 201, 404, etc
  statusCode: number, 
}

TStatus - Interface for relaying Data Access / CRUD operation results (or function output in general)

type TStatus = {
  // operation status code - i.e. 200, 201, 404, etc
  code: number,
  // short message describing result (e.g. 'Created successfully')
  message: string,
  // suggested fixes (optional e.g. contact support)
  fix?: string,
  // resultant data from operation (including pagination etc)
  data?: any,
  // error details (raw error object / instance)
  error?: any,
  // "OK" | "Created" | "BadRequest" | "Unauthorized" etc
  statusType: string
}

Usage

Generating ServiceResponse

  • Import and invoke the generator function (e.g. OK, NotFound, Created, Unauthorized etc) passing in none or any of the ServiceResponse type options (i.e. {message, success, data, fix, meta, error, errMessage, newAccessToken});
import express, { Request, Response } from 'express';
import { OK, NotFound, Unauthorized, ServiceResponse } from '@neoncoder/service-response'

const app = express();

// generate OK - 200 response
app.get('/endpoint', async (req: Request, res: Response) => {
  const someData = await getSomeData()
  const sr: ServiceReponse = OK({data: someData})
  res.status(sr.statusCode).send(sr)
})

// generate NotFound - 404 response
app.get('/not-found', async (req: Request, res: Response) => {
  const sr: ServiceReponse = NotFound({message: 'This route does not exist'})
  res.status(sr.statusCode).send(sr)
})

// generate Unauthorized - 401 response
app.get('/unauthorized', async (req: Request, res: Response) => {
  if(!checkUserAuth()){
    const sr: ServiceReponse = Unauthorized({})
    return res.status(sr.statusCode).send(sr)
  }
  next()
})
  • Or Use the Rez object and call the functions from there
import express, { Request, Response } from 'express';
import { Rez, ServiceResponse } from '@neoncoder/service-response'

const app = express();

// generate OK - 200 response
app.get('/endpoint', async (req: Request, res: Response) => {
  const someData = await getSomeData()
  const sr: ServiceReponse = Rez.OK({data: someData})
  res.status(sr.statusCode).send(sr)
})

// generate NotFound - 404 response
app.get('/not-found', async (req: Request, res: Response) => {
  const sr: ServiceReponse = Rez.NotFound({fix: 'check the route exists'})
  res.status(sr.statusCode).send(sr)
})

Generating TStatus

  • The statusMap.get(<statusCode>) method returns a function that takes in an options object as parameter (i.e.{message, fix, error, data}) to return a TStatus object;
import express, { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client'
import { statusMap, TStatus, Rez } from '@neoncoder/service-response'

const prisma = new PrismaClient()
const app = express();

app.get('/user/:id', async (req: Request, res: Response) => {
  let result: TStatus;
  try {
    const user = await prisma.user.findUnique({
      where: {
        id: req.params.id
      }
    })
    // if user is found return 200 with user data, else return 404
    result = user 
      ? statusMap.get(200)!({data: user}) 
      : statusMap.get(404)!({})
  } catch (error: any) {
    result = statusMap.get(500)!({error})
  }
  // Quickly generate a ServiceResponse from the TStatus object / result
  const sr: ServiceResponse = Rez[result.statusType]({...result})
  return res.status(sr.statusCode).send(sr)
})
  • The statuses object contains a predefined list of TStatus Objects that can be referenced by the Status Type. Using the previous example with the statuses object becomes:
import express, { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client'
import { statuses, TStatus, Rez } from '@neoncoder/service-response'

const prisma = new PrismaClient()
const app = express();

app.get('/user/:id', async (req: Request, res: Response) => {
  let result: TStatus;
  try {
    const user = await prisma.user.findUnique({
      where: {
        id: req.params.id
      }
    })
    // if user is found return 200 with user data, else return 404
    result = user 
      ? {...statuses.OK, data: user}
      : {...statuses.NotFound}
  } catch (error: any) {
    result = {...statuses.InternalServerError, error}
  }
  // Quickly generate a ServiceResponse from the TStatus object / result
  const sr: ServiceResponse = Rez[result.statusType]({...result})
  return res.status(sr.statusCode).send(sr)
})

List of Status Codes, Types & Generator Fxns

Status Code StatusType Generate ServiceResponse Generate TStatus
200 OK OK({}) || Rez.OK({}) statusMap.get(200)!({})
201 Created Created({}) || Rez.Created({}) statusMap.get(201)!({})
204 NoContent NoContent({}) || Rez.NoContent({}) statusMap.get(204)!({})
400 BadRequest BadRequest({}) || Rez.BadRequest({}) statusMap.get(400)!({})
401 Unauthorized Unauthorized({}) || Rez.Unauthorized({}) statusMap.get(401)!({})
403 Forbidden Forbidden({}) || Rez.Forbidden({}) statusMap.get(403)!({})
404 NotFound NotFound({}) || Rez.NotFound({}) statusMap.get(404)!({})
405 MethodNotAllowed MethodNotAllowed({}) || Rez.MethodNotAllowed({}) statusMap.get(405)!({})
408 TimeoutError TimeoutError({}) || Rez.TimeoutError({}) statusMap.get(408)!({})
415 UnsupportedMediaType UnsupportedMediaType({}) || Rez.UnsupportedMediaType({}) statusMap.get(415)!({})
417 ExpectationFailed ExpectationFailed({}) || Rez.ExpectationFailed({}) statusMap.get(417)!({})
422 UnprocessableEntity UnprocessableEntity({}) || Rez.UnprocessableEntity({}) statusMap.get(422)!({})
429 TooManyRequests TooManyRequests({}) || Rez.TooManyRequests({}) statusMap.get(429)!({})
500 InternalServerError InternalServerError({}) || Rez.InternalServerError({}) statusMap.get(500)!({})
503 ServiceUnavailable ServiceUnavailable({}) || Rez.ServiceUnavailable({}) statusMap.get(503)!({})
504 GatewayTimeout GatewayTimeout({}) || Rez.GatewayTimeout({}) statusMap.get(504)!({})

Other Utilities

The package also provides an IDataAccess interface for easy implementation of the TStatus type

interface IDataAccess {
  result: TStatus | undefined
}

An example usecase could be ensuring that a DBAL class returns data in a consistent shape. In the example below a class is used to manage a resource called Resource (could be a User, or Item, or Post etc)

// Data Object Class file
import {IDataAccess, TStatus, statusMap} from '@neoncoder/service-response'
import { PrismaClient, User, UserCreateInput PrismaClientValidationError, PrismaClientKnownRequestError } from '@prisma/client'

class UserDBALClass implements IDataAccess {

  user: User | undefined
  result: TSatus | undefined
  prisma: PrismaClient

  constructor(){
    this.prisma = new PrismaClient()
  }

  async create(data: UserCreateInput){
    try{
      const newUser = await prisma.user.create({data})
      this.result = statusMap.get(201)!({data: newUser})
      this.user = newUser
    } catch (err){
      this.formatError(err)
    } finally {
      return this
    }
  }

  async update(id: string, data: UserUpdateInput){
    try{
      const updatedUser = await prisma.user.update({where: {id}, data})
      this.result = statusMap.get(201)!({data: updatedUser})
    } catch (err){
      this.formatError(err)
    } finally {
      return this
    }
  }

  formatError(error: unknown, msg = 'An error occurred') {
    const message = error.message ?? msg
    if(
        err instanceof PrismaClientValidationError 
        ||err instanceof PrismaClientKnownRequestError
      ) {
        return this.result = statusMap.get(400)!({error, message});
      }
      this.result = statusMap.get(500)!({error, message})
  }
}

// In Some other file
import {Request, Response} from 'express';
app.post('/users', async (req: Request, res: Response) => {
  const {name, gender} = req.body
  const userA = new UserDBALClass()
  const {result} = await userA.create({name}).update(userA.user?.id, {gender})
  const sr: ServiceResponse = result 
    ? Rez[result.statusType]({...result})
    : Rez.InternalServerError({}) 
  return res.status(sr.statusCode).send(sr)
});