1.0.2 • Published 7 months ago

@sidev/api-handler v1.0.2

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

@sidev/

api-handler

A type-safe library for handling API responses and function returns in JS and TS applications.

Features

  • Provides consistent return values and type safety.
  • Standardizes success and error handling for better readability.
  • Integrates easily with TypeScript and JavaScript projects.
  • Supports custom data structures and application-specific types.

TOC

Installation

You can install @sidev/api-handler using npm or yarn:

npm install @sidev/api-handler

or

yarn add @sidev/api-handler

Usage

1. Basic Setup

Import the library and create your API handler:

import { createApiHandler } from '@sidev/api-handler'

const APP_ERRORS = {
  AUTH_MEMBER_EXISTS: {
    code: 'AUTH_MEMBER_EXISTS',
    message: 'Member already exists.',
    uiMessages: ['This member already exists.', 'Please log in instead.'],
  },
  AUTH_NO_CREDENTIALS: {
    code: 'AUTH_NO_CREDENTIALS',
    message: 'No credentials provided.',
    uiMessages: ['No credentials were provided.', 'Please try again.'],
  },
}

const { apiError, apiSuccess, ERROR, CODE } = createApiHandler(APP_ERRORS)

2. Handling Errors

You can generate error responses using apiError():

const errorInstance = new Error(CODE.AUTH_MEMBER_EXISTS)
const errorResponse1 = apiError(errorInstance)

console.log('apiError(errorInstance) returns:', errorResponse1)

// Using error code as a string
const errorCode = CODE.AUTH_MEMBER_EXISTS
const errorResponse2 = apiError(errorCode)

console.log('apiError(errorCode) returns:', errorResponse2)

Note:

/*
  When using `apiError` to throw error codes, you can either pass the error code string directly or wrap it in a new `Error` object.
  While both approaches work, wrapping the error code in a new `Error` object will provide a more accurate stack trace, showing the exact location where the error was thrown. This can be a minor but useful difference for debugging purposes.
 */
try {
  // Example of throwing an error with a more accurate stack trace
  throw new Error(CODE.AUTH_MEMBER_EXISTS)

  // Example of throwing an string with error code
  throw CODE.AUTH_MEMBER_EXISTS
  //
} catch (error) {
  // return from catch block
  return apiError(error)
}

3. Handling Success Responses

To generate success responses, use apiSuccess():

const dataObj = {
  data: { key: 'value' },
  message: 'Operation successful',
}
const successResponse = apiSuccess(dataObj)
console.log(successResponse)

4. Using in Functions

Here’s an example of using apiSuccess() and apiError() in a function:

import { createApiHandler } from '@sidev/api-handler'
import { myErrors } from './my-errors'

const { apiError, apiSuccess, CODE } = createApiHandler(myErrors)

const handleRequest = (condition: string) => {
  try {
    if (condition === 'error') {
      throw new Error(CODE.NOT_AUTHORIZED)
      // or
      // throw CODE.AUTH_NO_CREDENTIALS;
    }
    // go on
    return apiSuccess({
      data: { abc: 'xyz' },
      message: 'Request handled successfully',
    })
  } catch (error) {
    // return what was thrown with apiError()
    // if any other error happen it will be handled also uniformly
    // and return expected ApiErrorResponse object
    return apiError(error)
  }
}

console.log(handleRequest('error'))
/* logs:
{
  success: false,
  error: Error: Not authorized
      at handleRequest ... ...
  message: 'Not authorized',
  code: 'NOT_AUTHORIZED',
  uiMessages: [
    'You are Not authorized',
    'This content is only available to authorized users.',
    'Look for the login button in the top right corner.'
  ]
}
*/
console.log(handleRequest('success'))
/* logs:
{
  success: true,
  data: { abc: 'xyz' },
  message: 'Request handled successfully'
}
*/

5. Example with Custom Types (TS)

You can separate your error definitions in a centralized file, even for the whole app. Use type ApiErrorsMap

// my-errors.ts

import { type ApiErrorsMap } from '@sidev/api-handler'

// Centralized error definitions
export const myErrors: ApiErrorsMap = {
  NO_ACCOUNT: {
    code: 'NO_ACCOUNT',
    message: 'Account not found',
    uiMessages: [
      'Try creating an account.',
      'If you are an admin, please contact developer team.',
    ],
  },
  NOT_AUTHORIZED: {
    code: 'NOT_AUTHORIZED',
    message: 'Not authorized',
    uiMessages: [
      'You are Not authorized',
      'This content is only available to authorized users.',
      'Look for the login button in the top right corner.',
    ],
  },
  // ... more
}

// only for this example mock function
export async function simulatedDb(id: number) {
  await new Promise((resolve) => setTimeout(resolve, 30))
  /* --- 555 id will simulate unpredicted error from db --- */
  switch (id) {
    case 555:
      throw new Error('Some error outside your definitions')
    case 10:
      return { id, name: 'John Doe', role: 'Admin' }
    case 20:
      return { id, name: 'Smith Bob', role: 'Customer' }
    default:
      return {}
  }
}

Now, in module, you can define custom types for your .data with type ApiResponseWith

// example.ts

import { createApiHandler, type ApiResponseWith } from '@sidev/api-handler'
import { myErrors, simulatedDb } from './my-errors'

const { apiError, apiSuccess, CODE } = createApiHandler(myErrors)

type UserAccount = {
  id: number
  name: string
  role: string
}

const getAdminAccount = async (
  id: number
): Promise<ApiResponseWith<UserAccount>> => {
  try {
    const userAccount = await simulatedDb(id) // a mock function
    if (userAccount.id) {
      if (userAccount.role !== 'Admin') {
        throw new Error(CODE.NOT_AUTHORIZED)
      }
      return apiSuccess({
        /* --- data property is typechecking for UserAccount --- */
        data: userAccount,
        message: 'Account fetched successfully', // optional default "Success"
      })
    }
    throw new Error(CODE.NO_ACCOUNT)
  } catch (error) {
    return apiError(error)
  }
}

/* --- call it with different id param to test outputs --- */
let response
response = await getAdminAccount(20) // success:false NOT_AUTHORIZED
response = await getAdminAccount(10) // success:true
response = await getAdminAccount(555) // success:false UNEXPECTED ERROR
response = await getAdminAccount(1000) // success:false NO_ACCOUNT

if (response.success) {
  console.log('success response', response)
} else {
  console.log('error response', response)
  /* --- do something with uiMessages on frontend --- */
  // showToast(response.uiMessages[0])
  response.uiMessages.map((message: string) => console.log(message))
}

6. apiSuccess() Important notes and .warning key

Function always returns ApiSuccessResponse object. On bad data attaches warning

import { createApiHandler } from '@sidev/api-handler'

const { apiError, apiSuccess } = createApiHandler({})

/**
 * JS runtime check in: apiSucess({ data : ValidData })
 *
 * - Call to apiSuccess() always! returns success response.
 * - In case of misuse of apiSuccess params response.warning is attached.
 * - Bad params or value, is converted to String and returned in response.data.
 *
 * _________________________________________________________________
 */

console.log(apiSuccess()) // with {warning}
console.log(apiSuccess(null)) // with {warning}
/* logs:
{
  success: true,
  data: 'null',
  message: 'Success',
  warning: 'Bad params in apiSuccess(params). Params should be { data: value } object. Got null.'
}
*/
console.log(apiSuccess({ abc: 1 })) // with {warning}
console.log(apiSuccess(100)) // with {warning}

console.log(apiSuccess({ data: null, message: 'lorem ipsum' })) // with {warning}
console.log(apiSuccess({ data: undefined })) // with {warning}
console.log(apiSuccess({ data: 888 })) // with {warning}

console.log(apiSuccess({ data: 'lorem ipsum' })) // OK
console.log(apiSuccess({ data: [4, 5, 6] })) // OK
console.log(apiSuccess({ data: { abc: 2 } })) // OK

// ---- In apiError call with bad params will return with code: UNKNOWN_ERROR

console.log(apiError())
/* logs:
{
  success: false,
  error: Error: Unknown error occurred
  message: 'Unknown error occurred',
  code: 'UNKNOWN_ERROR',
  uiMessages: [ 'An unknown error occurred.' ]
}
*/

Types Overview

ApiHandler

Represents a unified API response interface that can be either success or error response.

declare function createApiHandler(apiErrors: ApiErrorsMap): ApiHandler

export type ApiHandler = {
  apiError: (error: unknown) => ApiErrorResponse
  apiSuccess: <T extends ValidData>(params: {
    data: T
    message?: string
  }) => ApiSuccessResponse<T>
  readonly ERROR: ApiErrorsMap
  readonly CODE: Record<string, string>
}

ValidData

Represents valid types of data that can be used in success responses. It can be an object, array, or string.

type ValidData = object | any[] | string

ApiResponseBase

The base structure for all API responses, containing a success flag and an optional message.

export type ApiResponseBase = {
  success: boolean
  message?: string
}

ApiSuccessResponse

Represents a successful API response. It extends ApiResponseBase and includes the data field containing the response data. Other error-specific fields are set to never.

export type ApiSuccessResponse<T extends ValidData = object> =
  ApiResponseBase & {
    success: true
    data: T
    warning?: string // internal for misuse of apiSuccess
    error?: never
    code?: never
    uiMessages?: never
  }

ApiErrorResponse

Represents an error response. It extends ApiResponseBase and includes additional fields for error, code, and uiMessages.

export type ApiErrorResponse = ApiResponseBase & {
  success: false
  error: Error
  code: string
  uiMessages: string[]
  data?: never
  warning?: never
}

ApiResponse

A union type that can be either a success or an error response.

export type ApiResponse = ApiSuccessResponse | ApiErrorResponse

ApiResponseWith

A generic type that extends ApiResponse to include specific data types in success responses.

export type ApiResponseWith<T extends ValidData> =
  | ApiSuccessResponse<T>
  | ApiErrorResponse

AppError

Defines the structure for application-specific errors, including code, message, and uiMessages.

export type AppError = {
  code: string
  message: string
  uiMessages: string[]
}

ApiErrorsMap

A record type that maps error codes to AppError objects.

export type ApiErrorsMap = Record<string, AppError>

API Reference

createApiHandler(apiErrors: ApiErrorsMap)

  • Description: Initializes an API handler with custom error configurations.
  • Parameters:
    • apiErrors: A map of application-specific error codes to error details, including messages and UI messages.
  • Returns: An API handler object containing methods for generating standardized success and error responses.

apiError(error: unknown): ApiErrorResponse

  • Description: Processes an error and returns a structured API error response.
  • Parameters:
    • error: The error instance or code to be transformed into an API error response.
  • Returns: An ApiErrorResponse object containing error details and UI messages.

apiSuccess<T extends ValidData>(params: { data: T; message?: string }): ApiSuccessResponse<T>

  • Description: Generates a standardized API success response.
  • Parameters:
    • params: An object containing:
      • data: The data to include in the success response.
      • message: An optional message to accompany the success response.
  • Returns: An ApiSuccessResponse<T> object with the provided data and message.

About

Author

si Slavko Ivanovic

License

Released under the MIT License.