4rest
TypeScript icon, indicating that this package has built-in type declarations

2.0.0 • Public • Published

4REST

test coverage npm version install size minizipped size License

4rest (Forest) is a promise based, HTTP REST Client built on top of axios and zod packages suggesting easy to use and extensively customizable, extendable and configurable services with built in CRUD methods and type safe request functions to API.


Installation

Using npm

  npm install 4rest

Using yarn

  yarn add 4rest

Table of Contents

Types

    Motivation

    The package was created to help developers to get their requests functions from client to API up and runing quickly and comfortably, as well as making configuration for requests all centeralized in one spot at the code base of the app. Using the package, you can divide your requests functions to API to different services based on the models (data structures) of the API. When you have initiallized your service, you can make request to API at every place in your app with type safety based on the service configuration.

    Note: The package was meant to be used with rest API only and it does not support graphql.


    Features

    💎 Service with built in CRUD Methods:

    • getAll
    • getById
    • deleteAll
    • deleteById
    • post
    • patch
    • patchById
    • put
    • putById

    🎨 Extended Services with option to add additional methods extending out CRUD methods that comes built in with the base service you create

    🧱 Services Built on fully configurable axios Instance

    ⚙️ Convenient Configuration with custom routes, request configuration, and payload data (body property of request) key custom define. all of that configuration is per service or can be set globally on fetching instance as well

    🛡️ Data Type Validation - API fetching requests, payloads and responses data validation with zod schemas

    🧪 Test Proof - 4rest has 100% test coverage


    Usage / Examples

    🧱Basic

    1) Create Instance

    import forest from "4rest";
    
    const instance = forest.create({ baseURL: "http://localhost:5000" });

    2) Create Service

    import { instance } from "./forestInstance";
    import { UserWithId, User } from "./types";
    
    const userService = instance.createService<UserWithId, User>("user");

    3) Use Service

    • GET

    // GET http://localhost:5000/user
    async function getUsers() {
      const users: User[] = await (await userService.getAll()).data;
    }
    
    // GET http://localhost:5000/user/:id
    async function getUserById(id: string) {
      const user: User = await (await userService.getById(id)).data;
    }
    • DELETE

    // DELETE http://localhost:5000/user
    async function deleteAllUsers() {
      const usersDeleted: User[] = await (await userService.deleteAll()).data;
    }
    
    // DELETE http://localhost:5000/user/:id
    async function deleteUserById(id: ObjectId) {
      const userDeleted: User = await (await userService.deleteById(id)).data;
    }
    • POST

    // POST http://localhost:5000/user
    async function createUser(newUser: User) {
      const userCreated: User = await (await userService.post(newUser)).data;
    }
    • PATCH

    // PATCH http://localhost:5000/user
    async function updateUser(partialUser: Partial<User>) {
      const updatedUser: User = await (await userService.patch(partialUser)).data;
    }
    
    // PATCH http://localhost:5000/user/:id
    async function updateUserById(id: ObjectId, partialUser: Partial<User>) {
      const updatedUser: User = await (await userService.patchById(id, partialUser)).data;
    }
    • PUT

    // PUT http://localhost:5000/user
    async function updateUser(partialUser: Partial<User>) {
      const updatedUser: User = await (await userService.put(partialUser)).data;
    }
    
    // PUT http://localhost:5000/user/:id
    async function updateUserById(id: ObjectId, partialUser: Partial<User>) {
      const updatedUser: User = await (await userService.putById(id, partialUser)).data;
    }

    🎨 Extended

    1) Create Extended Service

    import { ForestService } from "4rest";
    
    import { instance } from "./forestInstance";
    
    export class UserService extends ForestService<UserWithId, User> {
      constructor() {
        super("user", instance, {
          /* service config will go here */
        });
        /* prefix for request url will be "user" */
      }
    
      public getByName = (name: string) => this.methodsCreator.getByParam<UserWithId, string>({ suffix: "name" })(name);
      public getByNameWithQuery = (name: string) =>
        this.methodsCreator.get<UserWithId>({ route: "name", config: { params: { name } } })();
      public isEmailTaken = (email: string) =>
        this.methodsCreator.getByParam<boolean, string>({ route: ["email", "taken"] })(email);
    }

    Notes:

    1. You must include constructor in the structure that is shown above in your extended service in order for it to work properly

    2. Extended Service will include all the base service methods as well as the additional ones you have added

    3. Read about Methods Creator to add new methods to extended service easily

    2) Use Extended Service

    const userService = new UserService();
    
    async function getUserName(name: string) {
      const user: User = await (await userService.getByName(name)).data;
    }
    
    async function getUserNameByQuery(name: string) {
      const user: User = await (await userService.getByNameWithQuery(name)).data;
    }
    
    async function isEmailTaken(email: string) {
      const isEmailTaken: boolean = await (await userService.isEmailTaken(email)).data;
    }

    Configuration

    📀 Forest Instance

    Instance Creation

    Create Forest Instance based axios Instance with forest.create() Function

    import forest from "4rest";
    
    /* Customised Forest Instance can be based on
       AxiosInstance, AxiosRequestConfig */
    
    const forestInstance = forest.create({
        axiosSettings: /* Here goes instance or config*/,
        globalServiceConfig: /* Here goes service configuration that will be applied by default to
                                all created service from these ForestInstance*/
      });

    See What Service Config includes down below.

    Note: if a created service will have a config of it's own, it's config properties will be overriding the global service config properties, which means the more specific property is the one that will be in use eventually


    Options to configure forest.create()

    interface InstanceConfig {
      axiosSettings?: AxiosSettings;
      globalServiceConfig?: ServiceConfig;
    }

    Note: Configuration is completly optional, and if axiosSettings are empty the forest instance will be based on the base AxiosInstance

    Axios Instance Access

    You can access the totally regular AxiosInstance that the ForestInstance is based on which contains all of axios methods on it: Access it using the axiosInstance property on created ForestInstance

    import { forestInstance } from "./instance";
    
    const response = forestInstance.axiosInstance.get("localhost:5000/users" /* Here goes axios config*/);

    📀 Forest Service

    Configure Service with createService() Method:

    1) Methods REST Routes

    You may want to change few of the built in service method route to extend the prefix based on the API you are working with.

    Do it easily by configuring an extended route for each method you want.

    Note: method with no configured extended route will send request to basic route: baseUrl/prefix or baseUrl/prefix/param

    Example:

    import { instance } from "./forestInstance";
    
    const userService = instance.createService<User>("user", {
      /* All Service built in CRUD methods route control ( string | string[] ) */
      routes: {
        getAll: ["get", "all"], // GET http://localhost:5000/user/get/all
        deleteAll: "all",       // DELETE http://localhost:5000/user/all
        deleteById: "id",       // DELETE http://localhost:5000/user/id/:id
        ...,
        post: "create",         // POST http://localhost:5000/user/create
        patch: "update"         // PATCH http://localhost:5000/user/update
        patchById: "update"     // PATCH http://localhost:5000/user/update/:id
      }
    });

    2) Request Config

    You can set a requestConfig of type AxiosRequestConfig for attaching metadata to a request (like headers, params, etc.)

    requestConfig can be set for each method seperatly or make one general config for all methods

    Note: if a method has its own specific requestConfig, it will be used over the general one

    Example:

    import { instance } from "./forestInstance";
    
    const userService = instance.createService<UserWithId, User, number>("user", {
      requestConfigByMethod: {
        /* Request Config Per Method */
        getAll: { params: { page: 1, size: 10 } },
        getById: { maxRedirects: 3 },
      },
      requestConfig: {
        /* Request Config For All Methods */
        headers: {
          Authentication: "Bearer Header",
        },
      },
    });

    3) Payload Data Key

    For HTTP methods with payload (Post, Patch, Put) you can set a payloadKey for setting the payload data on the key you want inside the body of the request

    // By Default
    request: {
      body: data,
      ...
    }
    
    // After Setting payloadKey
    request: {
      body: {
        [payloadKey]: data
      },
      ...
    }

    payloadKey can be set for each HTTP payload method seperatly or set one general config for all methods

    Note: if a method has its own specific payloadKey, it will be used over the general one

    Example:

    import { instance } from "./forestInstance";
    
    const userService = instance.createService<UserWithId, User, number>("user", {
      payloadKey: "update",
      payloadKeyByMethod: { post: "data" },
    });

    4) OnSuccess / OnError Handling

    You can configure in advance how to handle each request when it is completes successfully or failing and throws error.

    Set up onSuccess function which parameters will be the response from successful request and optionally the metadata of the request.

    Set up onError function which parameters will be the error that was thrown from a failed request and optionally the metadata of the request.


    Without metadata parameter used:

    const userService = forestInstance.createService<UserWithId, User, number>("user", {
        onSuccess: (response) => response.data,
        onError: (error) => {
          return { error, msg: "error" };
        },
      });

    With metadata parameter used:

    const userService = forestInstance.createService<UserWithId, User, number>("user", {
        onSuccess: (response, metadata) => {
          console.log(metadata.serviceConfig.validation);
          return response.data
        },
        onError: (error, metadata) => {
          console.log(metadata.serviceConfig.routes);
          return { error, msg: "error" };
        },
      });

    You can also set different onSuccess and onError functions for each service method in the following way:

    const userService = forestInstance.createService<UserWithId, User, number>("user", {
        onSuccessByMethod: {
          getAll: (response) => response.data,
          getById: (response) => response.status,
          ...,
          post: (response, metadata) => {
            console.log(metadata.serviceFunction);
    
            return response.data;
          },
        },
        onErrorByMethod: {
          getById: (error) => {
            return { error, msg: "errorByMethod" };
          },
          ...,
          post: (error, metadata) => {
            console.log('error at method:', metadata.serviceFunction);
    
            throw error;
          },
        },
      });

    5) Zod Validation

    If you want to make sure that the data you send to the API or recieved back from it, matches your data model schemas you can set validation config for service with zod schemas.

    First of all, decalre your validation schemas using zod:

    export const UserSchema = z.object({
      name: z.string(),
      email: z.string().optional(),
    });
    
    export const UserWithIdSchema = UserSchema.extend({
      _id: z.number(),
    });

    then apply zod schemas to the fitting property of service configuration object:

    • Global validation for all methods on service:
    import { UserWithIdSchema, UserSchema } from "../../types/user";
    
    const userService = forestInstance.createService<UserWithId, User, number>("user", {
      validation: {
          types: { resoponseData: UserWithIdSchema },
        }
      }
    });

    Examples for data recieved back from API:

    // Valid Data, no error will be thrown
    [{ _id: 1, name: "John Smith" }];
    
    // Invalid data, will throw error
    [{ name: "John Smith" }];

    • Validation by service method:
    import { UserWithIdSchema, UserSchema } from "../../types/user";
    
    const userService = forestInstance.createService<UserWithId, User, number>("user", {
      validation: {
        onMethods: {
          post: { types: { requestPayload: UserSchema, resoponseData: UserWithIdSchema } },
          getById: { types: { resoponseData: UserSchema.strict() } },
        },
      },
    });

    Examples for payload sent to API:

    // Valid Data, no error will be thrown
    { name: "John Smith", email: "john.smith@gmail.com" };
    
    // Invalid data, will throw error
    { email: "john.smith@gmail.com" };

    • Combination of both:
    import { UserWithIdSchema, UserSchema } from "../../types/user";
    
    const userService = forestInstance.createService<UserWithId, User, number>("user", {
      validation: {
        onMethods: {
          post: { types: { requestPayload: UserSchema, resoponseData: UserWithIdSchema } },
          getById: { types: { resoponseData: UserSchema.strict() } },
        },
        types: { resoponseData: UserWithIdSchema },
      },
    });

    Note: if a method has its own specific validation, it will be used over the global one


    📀 Methods Creator Helper

    To help you construct new service methods, ForestService class comes included with property named methodsCreator that you can utilize to create new methods easily.

    methodsCreator property includes the following helper methods:

    • get
    • getByParam
    • delete
    • post
    • put
    • putByParam
    • patch
    • patchByParam

    Examples:

    public getByName = (name: string) => this.methodsCreator.getByParam<UserWithId, string>({ suffix: "name" })(name);
    public getByNameWithQuery = (name: string) => this.methodsCreator.get<UserWithId>({ route: "name", config: { params: { name } } })();
    public isEmailTaken = (email: string) => this.methodsCreator.getByParam<boolean, string>({ route: ["email", "taken"] })(email);

    Configuration Options:

    Base Helper Method:

    interface BaseConfig {
      route?: Route; // string or string[]
      config?: AxiosRequestConfig;
      serviceFunction?: ServiceFunction;
      validation?: MethodValidationConfig; // configuartion object based on Zod Schemas;
      onSuccess?: OnSuccessFunction;
      onError?: OnErrorFunction;
    }

    Payload Helper Method:

    interface PayloadConfig {
      route?: Route;
      config?: AxiosRequestConfig;
      validation?: MethodValidationConfig;
      onSuccess?: OnSuccessFunction;
      onError?: OnErrorFunction;
      key?: Key; // string
    }

    By Param Helper Method:

    interface ByParamConfig {
      route?: Route;
      suffix?: Route; // string or string[] added after the param in request url
      config?: AxiosRequestConfig;
      validation?: MethodValidationConfig;
      onSuccess?: OnSuccessFunction;
      onError?: OnErrorFunction;
    }

    Payload By Param Helper Method:

    interface PayloadByParamConfig {
      route?: Route;
      suffix?: Route;
      config?: AxiosRequestConfig;
      validation?: MethodValidationConfig;
      onSuccess?: OnSuccessFunction;
      onError?: OnErrorFunction;
      key?: Key;
    }

    Types

    Service Generics

    Service has generic types to control the following types of the service methods

    • Response Data
    • Payload Data
    • Id Type
    class ForestService<ResponseData = any, PayloadData = Response, IdType = string>

    By doing so, Typescript will force you to give it the parameters with matching types when calling the service methods or will recognize alone the response data type for more comfortable auto-completion in the future.

    You pass this generic types when creating new service with createService() function of a forestInstance

    Example:

    import { instance } from "./forestInstance";
    import { UserWithId, User } from "./types";
    
    const userService = instance.createService<UserWithId, User, string>("user");
    
    // ResponseData - UserWithId
    // PayloadData - User
    // IdType - string

    By default the service takes the types you passed to it and transform them to each service method in the following way:

    getAll

    • Response Data Type: ResponseData[]

    getById

    • Response Data Type: ResponseData
    • Id Type: IdType

    deleteAll

    • Response Data Type: ResponseData[]

    deleteById

    • Response Data Type: ResponseData
    • Id Type: IdType

    post

    • Response Data Type: ResponseData
    • Payload Data Type: PayloadData

    patch

    • Response Data Type: ResponseData
    • Payload Data Type: Partial<PayloadData>

    patchById

    • Response Data Type: ResponseData
    • Payload Data Type: Partial<PayloadData>
    • Id Type: IdType

    put

    • Response Data Type: ResponseData
    • Payload Data Type: Partial<PayloadData>

    putById

    • Response Data Type: ResponseData
    • Payload Data Type: Partial<PayloadData>
    • Id Type: IdType

    if you would like to change one or more of this method types, you can do it when calling the method by passing to it generic types that will be relevant to the this method only, at the place you are calling it.


    Example:

    Lets say you would like to change the type of the response data that comes back from calling to the post method from ResponseData to boolean because the API you working with is returns only with data that indicates whether or not an User has been created successfully

    You can do that in the following way:

    const data: boolean = await(await userService.post<boolean>(/* newUserData */)).data;

    Service Method Metadata

    Each request function (method) in 4rest includes metadata about the current request.

    Method metadata includes the following properties:

    interface Metadata {
      serviceConfig?: ServiceConfig; // current service config
      serviceFunction?: ServiceFunction; // name of one of the built in base service functions
    }

    OnSuccess Function

    Function For Handling Successful Requests:

    type OnSuccessFunction = <T>(value: AxiosResponse<T>, metadata?: Metadata) => any;

    OnError Function

    Function For Handling Failed Requests:

    type OnSuccessFunction = <T>(value: AxiosResponse<T>, metadata?: Metadata) => any;

    Validation

    ValidationTypes - validation can be set on either request payload or response data

    interface ValidationTypes<T = ZodSchema> {
      requestPayload?: T;
      resoponseData?: T;
    }

    ServiceValidationConfig - lets you set validation on each method seperatly via onMethods property or globaly for all methods via types property

    interface MethodsValidation {
      types: ValidationTypes;
    }
    
    interface ServiceValidationConfig {
      types: ValidationTypes;
      onMethods?: Partial<Record<ServiceFunction, MethodsValidation>>;
    }

    MethodValidationConfig - you can set validation specificlly on custom method you create for your extended service

    type MethodValidationConfig = ValidationTypes;

    License

    MIT

    Package Sidebar

    Install

    npm i 4rest

    Weekly Downloads

    19

    Version

    2.0.0

    License

    MIT

    Unpacked Size

    275 kB

    Total Files

    207

    Last publish

    Collaborators

    • liorvainer