effector-http-api
TypeScript icon, indicating that this package has built-in type declarations

2.5.0 • Public • Published

effector-http-api

Installation

Dependencies

npm i effector-http-api

or is you use yarn

yarn add effector-http-api

Also install peer-dependencies

yarn add axios effector

Usage

// src/shared/api/my-backend.ts`
import { createHttp } from 'effector-http-api'
import type { User, CreateUserDto, UpdateUserDto } from './models'

const instance = axios.create()
const http = createHttp(instance);

const routesConfig = http.createRoutesConfig({
 // createRoute<Dto, Contract>() returns Effect<Dto, Contract>
 getAll: http.createRoute<void, User[]>({
  url: '/',
  // "GET" method is set by default, no need to add this line
  method: 'GET'
 }),

 // dto provided to route pass to AxiosRequestConfig['data']
 create: http.createRoute<CreateUserDto, User>({
  url: '/',
  method: 'POST'
 }),

 // In 'GET' request `dto` will be passed to url as query
 getFiltered: http.createRoute<FiltersDto, User[]>({
  url: '/',
 }),

 // If you need to customize behavior of dto — use callback instead of config
 update: http.createRoute<{ id: User['id'], data: UpdateUserDto }, User>(({id, data}) => ({
  url: `/${id}`,
  method: 'PUT',
  data
 })),

 get: http.createRoute<User['id'], User>((id) => ({
  url: `/${id}`,
 })),


 createByUpload: http.createRoute<{ file: File }>({
  url: '/upload/users',
  method: 'POST',
  // dto provided with this flag converts to FormData under the hood
  formData: true
 })
});

const api = routesConfig.build();

export { api }

Headers

To attach headers to requests, call http.headers(unit), and pass as payload Unit<RawAxiosRequestHeaders>

http.headers(unit) it alias sample({clock: unit, target: $innerHttpHeaders }), if you need replace $innerHttpHeaders with your store (in case of defaultValues; or with effector-storage/local, which don't trigger $store.updates) You can pass custom store by createHttp(instance, $defaultHeaders)

Usage

import { createEvent, createStore } from "effector";
import { RawAxiosRequestHeaders } from "axios";


const headersChanged = createEvent<RawAxiosRequestHeaders>()
createHttp(instance).headers(headersChanged);

// or
const $headers = createStore<RawAxiosRequestHeaders>({})
createHttp(instance, $headers);

Options

To add custom option to route use http.createRoutesConfig.options(options)

batch: boolean

If route called multiple times while request is pending, calls will be batched with call which start a request

const http = createHttp(instance);

const routesConfig = http.createRoutesConfig({
 route: http.createRoute<void,User[]>({ url: '/' })
});

routesConfig.options({
 route: {
  batch: true
 }
});

const api = routesConfig.build();

Validation:

Validate response from backend before resolve request

yup validator example

import { number, object, string } from "yup";

const http = createHttp(instance);

const routesConfig = http.createRoutesConfig({
 getUsers: http.createRoute<void, User[]>({url: '/'}),
});

routesConfig.validation({
 getUsers: object({
  id: string().required(),
  age: number().required()
 }).required()
})


const api = routesConfig.build();

Custom validator example

import { ValidationSchema } from "effector-http-api";

const http = createHttp(instance);

const routesConfig = http.createRoutesConfig({
 getUsers: http.createRoute<void, User[]>({url: '/'}),
});

class MyCustomValidator implements ValidationSchema<User> {
 public validate = (user: User) => {
  //... validate user
  // return Promise.resolve() if passed
  // return Promise.reject() if not
 }
}

routesConfig.validation({
 getUsers: new MyCustomValidator()
})


const api = routesConfig.build();

Mock:

Configuration for return mock response instead calling request

const mockedUsers: User[] = [
 {id: 1, name: 'Mocked User 1'},
 {id: 2, name: 'Mocked User 2'}
]

const http = createHttp(instance);

const routesConfig = http.createRoutesConfig({
 getUsers: http.createRoute<void, User[]>({ url: '/' }),
 getUser: http.createRoute<User['id'], User>({ url: '/' }),
});

routesConfig.mocks({
 getUsers: {
   /**
   * Same as `options.batch`.
   * Usefull, then mock responseFn has timeouts or `mock.delay`
   */
  batch: true,
  
  delay: 1500,
  response: mockedUsers
 },
 getUser: {
  delay: 1000,
  response: (id) => mockedUsers.find(user => user.id === id)
 }
});

const api = routesConfig.build();

RawResponseFx

By default all routes map responses

(response: AxiosResponse) => response.data

Raw version of route, without mapper, accessible by prop on route

const http = createHttp(instance);

const routesConfig = http.createRoutesConfig({
  getData: http.createRoute<void, User[]>({ url:'/' }) 
});

const api = routesConfig.build();

api.getData // with mappings
api.getData.rawResponseFx // without mappings

BaseHttpFx

All routes attached to single baseRequestFx

Accessible from http.baseRequestFx

const http = createHttp(instance);

const requestWith401statusFailed = sample({
  clock: http.baseHttpFx.failData,
  filter: is401Error,
});

export { requestWith401statusFailed }

Instance

New http instance can be set by http.setHttpInstance(newHttpInstance)

//api/config.ts
const http = createHttp(instance);

export {http};

//setup-tests.ts
import { http } from 'api';
import { createMockHttpInstance } from 'tests/mocks'

http.setHttpInstance(createMockHttpInstance())

ForkAPI

See more examples in tests

//api/config.ts
const $instance = createStore(
    axios.create({ baseURL: 'https://api.com' }), 
    { serialize: 'ignore' }
);

const http = createHttp($instance);

export { http };

//pages/home.ts
const getServerSideProps = async () => {
    const newInstance = axios.create({ 
        baseURL: 'https://api.com' 
    });
    
    //interceptors only be created for this instance
    newInstance.interseptors.request.use(...);

    // replace store in fork creation phase
    const scope1 = fork({ values: [[http.$instance, newInstance]] });
    

    // or replace store by scoped event
    const scope2 = fork({});
    await allSettled(
        http.updateHttpInstance, 
        { scope: scope2, params: newInstance }
    );
    
    return {...}
}

Generate api layer from swagger

  1. Install codegen module
npm i swagger-typescript-api -D

or is you use yarn

yarn add swagger-typescript-api -D
  1. Create a script file
// {ROOT}/scripts/codegen.js

const {generateApi} = require('swagger-typescript-api');
const path = require('path');

const fileName = 'api.gen.ts';
const outputDir = path.resolve(process.cwd(), './src/shared/api');
const urlToSwaggerSchema = 'https://backend.com/swagger.json';

const pathToTemplate = path.resolve(process.cwd(), 'node_modules', 'effector-http-api/codegen-template');

generateApi({
 name: fileName,

 output: outputDir,

 url: urlToSwaggerSchema,

 httpClientType: 'axios',

 generateClient: true,

 templates: pathToTemplate,
});
  1. Create a config file
// {ROOT}/src/shared/api/config.ts

import axios from "axios";
import { createHttp } from 'effector-http-api'

const instance = axios.create();

const http = createHttp(instance);

export { http }
  1. Run this command to generate api layer
node ./scripts/codegen.js
  1. Check generated file at src/shared/api/api.gen.ts

  2. Add headers, options, mock, validation. Build routes and export api ready for usage


Babel-preset

//babel.config.js

module.exports = {
  presets: ["effector-http-api/babel-preset"]
}

Package Sidebar

Install

npm i effector-http-api

Weekly Downloads

104

Version

2.5.0

License

ISC

Unpacked Size

42.9 kB

Total Files

20

Last publish

Collaborators

  • nazarii-shvets