├── CONTRIBUTING.md ├── docs ├── .nojekyll ├── _navbar.md ├── simplified-syntax.md ├── installation.md ├── examples │ ├── service.md │ ├── locale-middleware.md │ ├── promise.md │ ├── auth-middleware.md │ ├── es5.md │ ├── browser.md │ └── testing.md ├── _sidebar.md ├── index.html ├── api │ ├── Service.md │ └── methods.md └── README.md ├── .npmignore ├── .babelrc ├── .eslintignore ├── .prettierrc ├── test ├── .eslintrc ├── mocks │ └── MiddlewareMock.js └── specs │ └── service.spec.js ├── src ├── index.js ├── index.esm.js └── service.js ├── .vscode └── settings.json ├── .editorconfig ├── .travis.yml ├── jest.conf.js ├── .gitignore ├── README.md ├── LICENSE ├── .eslintrc.js └── package.json /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src/ 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | 5 | package-lock.json 6 | 7 | !**/.* 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "globals": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Service from './service'; 2 | 3 | export default { 4 | Service, 5 | version: '__VERSION__', 6 | }; 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/index.esm.js: -------------------------------------------------------------------------------- 1 | import Service from './service'; 2 | 3 | export default { 4 | Service, 5 | version: '__VERSION__', 6 | }; 7 | 8 | export { Service }; 9 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | - [Github](https://github.com/emileber/axios-middleware) 2 | - [![npm version](https://badge.fury.io/js/axios-middleware.svg)](https://www.npmjs.com/package/axios-middleware) 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /docs/simplified-syntax.md: -------------------------------------------------------------------------------- 1 | # Simplified syntax for middlewares 2 | 3 | Instead of creating a class, you can use a simple object literal only implementing the [methods](api/methods.md) you need. 4 | 5 | ```javascript 6 | service.register({ 7 | onRequest(config) { 8 | // handle the request 9 | return config; 10 | } 11 | }); 12 | ``` 13 | -------------------------------------------------------------------------------- /test/mocks/MiddlewareMock.js: -------------------------------------------------------------------------------- 1 | export default class MiddlewareMock { 2 | constructor() { 3 | Object.assign(this, { 4 | onRequest: jest.fn((config) => config), 5 | onRequestError: jest.fn(), 6 | onSync: jest.fn((promise) => promise), 7 | onResponse: jest.fn((response) => response), 8 | onResponseError: jest.fn(), 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: jammy 2 | language: node_js 3 | node_js: 4 | - node 5 | notifications: 6 | email: false 7 | install: 8 | - npm ci 9 | before_script: 10 | # Needs to build before linting because a test file imports the built file. 11 | - npm run build:dev 12 | script: 13 | - npm run lint 14 | - npm run test:coverage 15 | cache: 16 | directories: 17 | - '$HOME/.npm' 18 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Using npm 4 | 5 | ``` 6 | npm install --save axios-middleware 7 | ``` 8 | 9 | ## Using a CDN 10 | 11 | Since Axios can be used in the browser directly, this plugin can also be dropped on a webpage and used as-is. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/examples/service.md: -------------------------------------------------------------------------------- 1 | # Exposing a service instance 2 | 3 | ```javascript 4 | import axios from 'axios'; 5 | import { Service } from 'axios-middleware'; 6 | import i18n from './i18n'; 7 | import { LocaleMiddleware, OtherMiddleware } from './middlewares'; 8 | 9 | // Create a new service instance 10 | const service = new Service(axios); 11 | 12 | // Then register your middleware instances. 13 | service.register([ 14 | new LocaleMiddleware(i18n), 15 | new OtherMiddleware() 16 | ]); 17 | 18 | export default service; 19 | ``` 20 | -------------------------------------------------------------------------------- /jest.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | rootDir: path.resolve(__dirname), 5 | moduleFileExtensions: ['js', 'json'], 6 | // moduleNameMapper: { 7 | // '^@/(.*)$': '/src/$1', // 'src' directory alias 8 | // '^~/(.*)$': '/test/$1', // 'test' directory alias 9 | // }, 10 | // transform: { 11 | // '^.+\\.js$': '/node_modules/babel-jest', 12 | // }, 13 | // setupFiles: ['/test/setup'], 14 | // mapCoverage: true, 15 | coverageDirectory: '/coverage', 16 | collectCoverageFrom: ['src/**/*.js', '!**/node_modules/**'], 17 | }; 18 | -------------------------------------------------------------------------------- /docs/examples/locale-middleware.md: -------------------------------------------------------------------------------- 1 | # Locale middleware 2 | 3 | Here's a simple example of a locale middleware who sets a language header on each request. 4 | 5 | ```javascript 6 | export default class LocaleMiddleware { 7 | constructor(i18n) { 8 | this.i18n = i18n; 9 | } 10 | 11 | onRequest(config) { 12 | // returns a new Object to avoid changing the config object referenced. 13 | return { 14 | ...config, 15 | headers: { 16 | // default `locale`, can still be overwritten by config.headers.locale 17 | locale: this.i18n.lang, 18 | ...config.headers 19 | } 20 | }; 21 | } 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - Introduction 2 | - [Getting started]() 3 | - [Installation](installation.md) 4 | - [Simplified syntax](simplified-syntax.md) 5 | 6 | - API 7 | - [Middleware methods](api/methods.md) 8 | - [`Service` class](api/Service.md) 9 | 10 | - Examples 11 | - [Auth retry middleware](examples/auth-middleware.md) 12 | - [Locale middleware](examples/locale-middleware.md) 13 | - [Service module](examples/service.md) 14 | - [Returning promises](examples/promise.md) 15 | - [ES5 usage](examples/es5.md) 16 | - [Browser usage](examples/browser.md) 17 | - [Testing a middleware](examples/testing.md) 18 | 19 | - Ecosystem 20 | - [Github](https://github.com/emileber/axios-middleware) 21 | - [npm](https://www.npmjs.com/package/axios-middleware) 22 | -------------------------------------------------------------------------------- /docs/examples/promise.md: -------------------------------------------------------------------------------- 1 | # Returning promises 2 | 3 | Every method of our middleware are promise callback functions, meaning that they can return either a value, a new promise or throw an error and the middleware chain will react accordingly. 4 | 5 | ```javascript 6 | export default class DemoPromiseMiddleware { 7 | onRequest(config) { 8 | return asyncChecks().then(() => config); 9 | } 10 | 11 | onResponseError({ config } = {}) { 12 | if (config && !config.hasRetriedRequest) { 13 | // Retrying the request 14 | return this.http({ 15 | ...config, 16 | hasRetriedRequest: true, 17 | }) 18 | .catch(function (error) { 19 | console.log('Retry failed:', error); 20 | throw error; 21 | }); 22 | } 23 | throw err; 24 | } 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # project 2 | /dist/ 3 | /coverage/ 4 | .eslintrc.json 5 | 6 | # IDE and OS 7 | .DS_Store 8 | .idea/ 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules/ 28 | jspm_packages/ 29 | 30 | # Typescript v1 declaration files 31 | typings/ 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional eslint cache 37 | .eslintcache 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | # Output of 'npm pack' 43 | *.tgz 44 | 45 | # Yarn Integrity file 46 | .yarn-integrity 47 | 48 | # dotenv environment variables file 49 | .env 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axios-middleware 2 | 3 | [![Build Status](https://travis-ci.org/emileber/axios-middleware.svg?branch=master)](https://travis-ci.org/emileber/axios-middleware) 4 | [![npm version](https://badge.fury.io/js/axios-middleware.svg)](https://www.npmjs.com/package/axios-middleware) 5 | [![codecov](https://codecov.io/gh/emileber/axios-middleware/branch/master/graph/badge.svg)](https://codecov.io/gh/emileber/axios-middleware) 6 | 7 | 8 | Simple [axios](https://github.com/axios/axios) HTTP middleware service. 9 | 10 | ## Installation 11 | 12 | ``` 13 | npm install --save axios-middleware 14 | ``` 15 | 16 | ## How to use 17 | 18 | Explore [**the documentation**](https://emileber.github.io/axios-middleware/) or the `docs/` directory. 19 | 20 | ## Contributing 21 | 22 | 23 | 24 | ### Updating the documentation 25 | 26 | The documentation is only static files in the `docs` directory. It uses [docsify](https://docsify.js.org/#/). 27 | 28 | ``` 29 | npm run docs 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/examples/auth-middleware.md: -------------------------------------------------------------------------------- 1 | # Unauthorized requests retry middleware 2 | 3 | In a case where we'd like to retry a request if not authenticated, we could return a promise in the `onResponseError` method. 4 | 5 | ```javascript 6 | export default class AuthMiddleware { 7 | constructor(auth, http) { 8 | this.auth = auth; 9 | this.http = http; 10 | } 11 | 12 | onResponseError(err) { 13 | if (err.response.status === 401 && err.config && !err.config.hasRetriedRequest) { 14 | return this.auth() 15 | // Retrying the request now that we're authenticated. 16 | .then((token) => this.http({ 17 | ...err.config, 18 | hasRetriedRequest: true, 19 | headers: { 20 | ...err.config.headers, 21 | Authorization: `Bearer ${token}` 22 | } 23 | })) 24 | .catch((error) => { 25 | console.log('Refresh login error: ', error); 26 | throw error; 27 | }); 28 | } 29 | throw err; 30 | } 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Emile Bergeron 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | env: { 6 | browser: true, 7 | }, 8 | extends: ['airbnb-base', 'plugin:prettier/recommended'], 9 | plugins: ['prettier'], 10 | // add your custom rules here 11 | rules: { 12 | 'class-methods-use-this': 'off', 13 | 'no-duplicate-imports': 'error', 14 | // risk only exist with semi-colon auto insertion. Not our case. 15 | 'no-plusplus': 'off', 16 | 'no-param-reassign': 'off', 17 | 'no-underscore-dangle': [ 18 | 'error', 19 | { 20 | allowAfterSuper: true, 21 | allowAfterThis: true, 22 | }, 23 | ], 24 | 'prefer-destructuring': 'off', 25 | // don't require .js extension when importing 26 | 'import/extensions': ['error', 'always', { js: 'never' }], 27 | // allow optionalDependencies 28 | 'import/no-extraneous-dependencies': [ 29 | 'error', 30 | { 31 | optionalDependencies: ['test/unit/index.js'], 32 | }, 33 | ], 34 | 'import/prefer-default-export': 'off', 35 | // allow debugger during development 36 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /docs/examples/es5.md: -------------------------------------------------------------------------------- 1 | # Usage with ES5 in Node 2 | 3 | ES5 doesn't have [classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) so you can't use the easy extend syntax sugar. Creating a new middleware from a base middleware class can still be done with a typical prototype based inheritance pattern. 4 | 5 | Create a custom middleware to register. 6 | 7 | ```javascript 8 | var BaseMiddleware = require('./base-middleware'); 9 | 10 | function MyMiddleware() { 11 | // call the parent constructor 12 | BaseMiddleware.apply(this, arguments); 13 | } 14 | 15 | // Prototype wiring 16 | var proto = MyMiddleware.prototype = Object.create(BaseMiddleware.prototype); 17 | proto.constructor = MyMiddleware; 18 | 19 | // Method overriding 20 | proto.onRequest = function(config) { 21 | // handle the request 22 | return config; 23 | }; 24 | 25 | module.exports = MyMiddleware; 26 | ``` 27 | 28 | Then export the service. 29 | 30 | ```javascript 31 | var axios = require('axios'), 32 | Service = require('axios-middleware').Service, 33 | MyMiddleware = require('./MyMiddleware'); 34 | 35 | // Create a new service instance 36 | var service = new Service(axios); 37 | 38 | // Then register your middleware instances. 39 | service.register(new MyMiddleware()); 40 | 41 | module.exports = service; 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Axios middleware documentation 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/api/Service.md: -------------------------------------------------------------------------------- 1 | # Middleware `Service` class 2 | 3 | This is the heart of this plugin module. It works by leveraging axios' adapter to call the middleware stack at each relevant steps of a request lifecycle. 4 | 5 | ## `constructor(axios)` 6 | 7 | You can pass an optional axios instance (the global one, or a local instance) on which to register the middlewares. If you don't pass an axios instance right away, you can use the `setHttp` method later on. 8 | 9 | Even if no axios instance was passed, you can still register middlewares. 10 | 11 | ## `setHttp(axios)` 12 | 13 | Sets or replaces the axios instance on which to intercept the requests and responses. 14 | 15 | ## `unsetHttp()` 16 | 17 | Removes the registered interceptors on the axios instance, if any were set. 18 | 19 | !> Be aware that changing the default adapter after the middleware service was initialized, then calling `unsetHttp` or `setHttp` will set the default adapter back in the axios instance. Any adapter used after could be lost. 20 | 21 | ## `has(middleware)` 22 | 23 | Returns `true` if the passed `middleware` instance is within the stack. 24 | 25 | ## `register(middlewares)` 26 | 27 | Adds a middleware instance or an array of middlewares to the stack. 28 | 29 | You can pass a class instance or a simple object implementing only the functions you need (see the [simplified syntax](simplified-syntax.md)). 30 | 31 | !> Throws an error if a middleware instance is already within the stack. 32 | 33 | ## `unregister(middleware)` 34 | 35 | Removes a middleware instance from the stack. 36 | 37 | ## `reset()` 38 | 39 | Empty the middleware stack. 40 | 41 | ## `adapter(config)` 42 | 43 | The adapter function that replaces the default axios adapter. It calls the default implementation and passes the resulting promise to the middleware stack's `onSync` method. 44 | -------------------------------------------------------------------------------- /docs/api/methods.md: -------------------------------------------------------------------------------- 1 | # The middleware methods 2 | 3 | These will be called at different step of a request lifecycle. Each method can return a promise which will be resolved or reject before continuing through the middleware stack. 4 | 5 | ?> **Any function is optional** and should be provided within a custom middleware only if needed. 6 | 7 | ## `onRequest(config)` 8 | 9 | Receives the configuration object before the request is made. Useful to add headers, query parameters, validate data, etc. 10 | 11 | !> It must return the received `config` even if no changes were made to it. It can also return a promise that should resolve with the config to use for the request. 12 | 13 | ## `onRequestError(error)` 14 | 15 | No internet connection right now? You might end up in this function. Do what you need with the error. 16 | 17 | You can return a promise, or throw another error to keep the middleware chain going. 18 | 19 | !> Failing to return a rejecting promise or throw an error would mean that the error was dealt with and the chain would continue on a success path. 20 | 21 | ## `onSync(promise)` 22 | 23 | The request is being made and its promise is being passed. Do what you want with it but be sure to **return a promise**, be it the one just received or a new one. 24 | 25 | ?> Useful to implement a sort of loading indication based on all the unresolved requests being made. 26 | 27 | ## `onResponse(response)` 28 | 29 | Parsing the response can be done here. Say all responses from your API are returned nested within a `_data` property? 30 | 31 | ```javascript 32 | onResponse(response) { 33 | return response._data; 34 | } 35 | ``` 36 | 37 | !> The original `response` object, or _a new/modified one_, or a promise should be returned. 38 | 39 | ## `onResponseError(error)` 40 | 41 | Not authenticated? Server problems? You might end up here. Do what you need with the error. 42 | 43 | ?> You can return a promise, which is useful when you want to retry failed requests. 44 | 45 | !> Failing to return a rejecting promise or throw an error would mean that the error was dealt with and the chain would continue on a success path. 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axios-middleware", 3 | "version": "0.4.0", 4 | "description": "Simple axios HTTP middleware service", 5 | "author": "Emile Bergeron ", 6 | "license": "MIT", 7 | "main": "dist/axios-middleware.common.js", 8 | "module": "dist/axios-middleware.esm.js", 9 | "unpkg": "dist/axios-middleware.js", 10 | "bugs": { 11 | "url": "https://github.com/emileber/axios-middleware/issues" 12 | }, 13 | "homepage": "https://emileber.github.io/axios-middleware/#/", 14 | "scripts": { 15 | "clean": "rimraf dist", 16 | "prebuild": "npm run clean", 17 | "prepublishOnly": "run-s build validate", 18 | "validate": "pkg-ok", 19 | "build": "node build/build.main.js", 20 | "build:dev": "rollup -c build/rollup.dev.config.js", 21 | "build:lint-config": "eslint -c .eslintrc.js --print-config .eslintrc.js > .eslintrc.json", 22 | "release": "np", 23 | "test": "run-p lint test:unit", 24 | "pretest:unit": "npm run build:dev", 25 | "test:unit": "jest", 26 | "pretest:coverage": "npm run test:unit -- --coverage", 27 | "test:coverage": "codecov", 28 | "lint": "eslint ./", 29 | "docs": "npx docsify-cli serve ./docs" 30 | }, 31 | "files": [ 32 | "dist" 33 | ], 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/emileber/axios-middleware.git" 37 | }, 38 | "keywords": [ 39 | "axios", 40 | "request", 41 | "adapter", 42 | "middleware", 43 | "response", 44 | "http" 45 | ], 46 | "peerDependencies": { 47 | "axios": ">=0.17.1 <1.2.0" 48 | }, 49 | "devDependencies": { 50 | "@babel/preset-env": "^7.3.4", 51 | "axios": "^1.1.3", 52 | "axios-mock-adapter": "^1.21.5", 53 | "codecov": "^3.2.0", 54 | "cross-env": "^7.0.3", 55 | "eslint": "^8.46.0", 56 | "eslint-config-airbnb-base": "^15.0.0", 57 | "eslint-config-prettier": "^9.0.0", 58 | "eslint-plugin-import": "^2.28.0", 59 | "eslint-plugin-prettier": "^5.0.0", 60 | "jest": "^29.6.2", 61 | "np": "^8.0.4", 62 | "npm-run-all": "^4.1.5", 63 | "pkg-ok": "^2.3.1", 64 | "prettier": "^3.0.1", 65 | "rimraf": "^2.6.3", 66 | "rollup": "^1.6.0", 67 | "rollup-plugin-buble": "^0.19.6", 68 | "rollup-plugin-replace": "^2.1.0" 69 | }, 70 | "engines": { 71 | "node": ">=0.10.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Axios HTTP middleware service 2 | 3 | Simple [axios](https://github.com/axios/axios) HTTP middleware service to simplify hooking (and testing of hooks) to HTTP requests made through Axios. 4 | 5 | ## What's this? 6 | 7 | A [`HttpMiddlewareService`](api/Service.md) which manages a middleware stack and hooking itself to an axios instance. 8 | 9 | Middlewares are just objects or classes composed of simple methods for different points in a request lifecycle. 10 | 11 | It works with either the global axios or a local instance. 12 | 13 | ## Why not use interceptors? 14 | 15 | Using axios interceptors makes the code tightly coupled to axios and harder to test. 16 | 17 | This middleware service module: 18 | 19 | - offers more functionalities (e.g. see [`onSync`](api/methods?id=onsyncpromise)) 20 | - looser coupling to axios 21 | - really easy to test middleware classes 22 | 23 | It improves readability and reusability in a centralized hooking strategy. 24 | 25 | ## Examples 26 | 27 | All examples are written using ES6 syntax but you can definitely use this plugin with ES5 code, even directly in the browser. 28 | 29 | ### Simplest use-case 30 | 31 | ?> A common use-case would be to expose an instance of the service which consumes an _axios_ instance configured for an API. It's then possible to register middlewares for this API at different stages of the initialization process of an application. 32 | 33 | The following example is using the [simplified syntax](simplified-syntax.md). 34 | 35 | ```javascript 36 | import axios from 'axios'; 37 | import { Service } from 'axios-middleware'; 38 | 39 | const service = new Service(axios); 40 | 41 | service.register({ 42 | onRequest(config) { 43 | console.log('onRequest'); 44 | return config; 45 | }, 46 | onSync(promise) { 47 | console.log('onSync'); 48 | return promise; 49 | }, 50 | onResponse(response) { 51 | console.log('onResponse'); 52 | return response; 53 | } 54 | }); 55 | 56 | console.log('Ready to fetch.'); 57 | 58 | // Just use axios like you would normally. 59 | axios('https://jsonplaceholder.typicode.com/posts/1') 60 | .then(({ data }) => console.log('Received:', data)); 61 | ``` 62 | 63 | It should output: 64 | 65 | ``` 66 | Ready to fetch. 67 | onRequest 68 | onSync 69 | onResponse 70 | Received: {userId: 1, id: 1, title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", body: "quia et suscipit↵suscipit recusandae consequuntur …strum rerum est autem sunt rem eveniet architecto"} 71 | ``` 72 | 73 | [**Demo snippet**](https://jsfiddle.net/emileber/sfqo0rt1/5/) 74 | -------------------------------------------------------------------------------- /docs/examples/browser.md: -------------------------------------------------------------------------------- 1 | # Usage in the browser 2 | 3 | The _axios-middleware_ plugin is made to be easily used in the browser right away. 4 | 5 | ## Common usage 6 | 7 | Just use the short middleware syntax to quickly define one-time use middleware. 8 | 9 | ```html 10 | 11 | 12 | 28 | ``` 29 | 30 | 31 | ## Advanced usage 32 | 33 | A mix of the ES5 usage within a common namespacing pattern for an app. 34 | 35 | Define your middlewares within a `Middleware.js` file. 36 | 37 | ```javascript 38 | // Middleware.js 39 | var app = app || {}; 40 | 41 | /** 42 | * Custom Middleware class 43 | */ 44 | app.MyMiddleware = (function(){ 45 | function MyMiddleware() {} 46 | 47 | var proto = MyMiddleware.prototype = Object.create(); 48 | 49 | proto.constructor = MyMiddleware; 50 | proto.onRequest = function(config) { 51 | // handle the request 52 | return config; 53 | }; 54 | return MyMiddleware; 55 | })(); 56 | ``` 57 | 58 | Then register these middlewares with a newly created `HttpMiddlewareService` instance. 59 | 60 | ```javascript 61 | // Service.js 62 | var app = app || {}; 63 | 64 | /** 65 | * Middleware Service 66 | */ 67 | app.MiddlewareService = (function(MiddlewareService, MyMiddleware) { 68 | // Create a new service instance 69 | var service = new MiddlewareService(axios); 70 | 71 | // Then register your middleware instances. 72 | service.register(new MyMiddleware()); 73 | 74 | return service; 75 | })(AxiosMiddleware.Service, app.MyMiddleware); 76 | ``` 77 | 78 | In this case, the order in which to import the JS files is important. 79 | 80 | ```html 81 | 82 | 83 | 84 | 85 | ``` 86 | 87 | ?> At that point, you may realise that it's a lot of files. You might want to think about using a bundler like [Webpack](https://webpack.js.org/), [Rollup](https://rollupjs.org/guide/en), or a simple concatenation pipeline with [grunt](https://gruntjs.com/) or [gulp](https://gulpjs.com/). 88 | -------------------------------------------------------------------------------- /docs/examples/testing.md: -------------------------------------------------------------------------------- 1 | # Middleware spec 2 | 3 | One of the great feature of this module is the ability to create easy-to-test self-contained middlewares. In order to be up to the task, it's still needed to design the middlewares in a way that makes testing easy. 4 | 5 | One rule that we should follow when implementing a middleware class is to use dependency injection over locally imported modules. 6 | 7 | ```javascript 8 | /** 9 | * When a request fails, this middleware adds a toast message using the 10 | * injected toast service. 11 | */ 12 | export default class ApiErrorMiddleware { 13 | /** 14 | * @param {Object} i18n 15 | * @param {Object} toast message service 16 | */ 17 | constructor(i18n, toast) { 18 | this.toast = toast; 19 | this.i18n = i18n; 20 | } 21 | 22 | /** 23 | * @param {Object} err 24 | */ 25 | onResponseError(err = {}) { 26 | let key = 'errors.default'; 27 | const { response } = err; 28 | 29 | if (response && this.i18n.te(`errors.${response.status}`)) { 30 | key = `errors.${response.status}`; 31 | } else if (err.message === 'Network Error') { 32 | key = 'errors.network-error'; 33 | } 34 | 35 | this.toast.error(this.i18n.t(key)); 36 | throw err; 37 | } 38 | } 39 | ``` 40 | 41 | Then, this is a spec example using [Jest](https://jestjs.io/). 42 | 43 | ```javascript 44 | import ApiErrorMiddleware from '@/middlewares/ApiErrorMiddleware'; 45 | 46 | let hasKey = false; 47 | 48 | // Simple translation mock, making it easier to compare results. 49 | const i18n = { 50 | t: key => key, 51 | te: () => hasKey, 52 | }; 53 | 54 | const errors = { 55 | unhandledCode: { response: { status: 999 } }, 56 | notfound: { response: { status: 404 } }, 57 | unhandledMessage: { message: 'test message' }, 58 | networkError: { message: 'Network Error' }, 59 | }; 60 | 61 | describe('ApiErrorMiddleware', () => { 62 | let toast; 63 | let middleware; 64 | 65 | /** 66 | * Jest needs a function when we're expecting an error to be thrown. 67 | * 68 | * @param {Object} err 69 | * @return {function(): *} 70 | */ 71 | function onResponseError(err) { 72 | return () => middleware.onResponseError(err); 73 | } 74 | 75 | beforeEach(() => { 76 | toast = { error: jest.fn() }; 77 | middleware = new ApiErrorMiddleware(i18n, toast); 78 | }); 79 | 80 | it('sends a default error message if not handled', () => { 81 | expect(onResponseError()).toThrow(); 82 | expect(toast.error).toHaveBeenLastCalledWith('errors.default'); 83 | 84 | expect(onResponseError(errors.unhandledCode)).toThrow(); 85 | expect(toast.error).toHaveBeenLastCalledWith('errors.default'); 86 | 87 | expect(onResponseError(errors.unhandledMessage)).toThrow(); 88 | expect(toast.error).toHaveBeenLastCalledWith('errors.default'); 89 | }); 90 | 91 | it('sends a code error message', () => { 92 | hasKey = true; 93 | expect(onResponseError(errors.notfound)).toThrow(); 94 | expect(toast.error).toHaveBeenLastCalledWith('errors.404'); 95 | }); 96 | 97 | it('sends a network error message', () => { 98 | hasKey = false; 99 | expect(onResponseError(errors.networkError)).toThrow(); 100 | expect(toast.error).toHaveBeenLastCalledWith('errors.network-error'); 101 | }); 102 | }); 103 | ``` 104 | -------------------------------------------------------------------------------- /test/specs/service.spec.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | import { Service } from '../../dist/axios-middleware.common'; 4 | import MiddlewareMock from '../mocks/MiddlewareMock'; 5 | 6 | const http = axios.create(); 7 | const mock = new MockAdapter(http); 8 | 9 | describe('Middleware service', () => { 10 | const service = new Service(http); 11 | 12 | afterEach(() => { 13 | mock.reset(); 14 | service.reset(); 15 | }); 16 | 17 | afterAll(() => { 18 | mock.restore(); 19 | }); 20 | 21 | it('works with the global axios instance', () => { 22 | expect.assertions(3); 23 | 24 | const axiosAdapter = axios.defaults.adapter; 25 | const globalMock = new MockAdapter(axios); 26 | const globalService = new Service(axios); 27 | 28 | globalMock.onAny().reply((config) => [200, config.param]); 29 | 30 | return axios().then(() => { 31 | expect(axios.defaults.adapter).not.toBe(axiosAdapter); 32 | expect(axios.defaults.adapter).toBeInstanceOf(Function); 33 | 34 | globalService.unsetHttp(); 35 | globalMock.restore(); 36 | 37 | expect(axios.defaults.adapter).toBe(axiosAdapter); 38 | }); 39 | }); 40 | 41 | it('throws when adding the same middleware instance', () => { 42 | const middleware = {}; 43 | 44 | service.register(middleware); 45 | 46 | expect(() => service.register(middleware)).toThrow(); 47 | }); 48 | 49 | it('works with both middleware syntaxes', () => { 50 | expect.assertions(2); 51 | const middleware = new MiddlewareMock(); 52 | const simplifiedSyntax = { 53 | onRequest: jest.fn((config) => config), 54 | }; 55 | 56 | service.register([middleware, simplifiedSyntax]); 57 | 58 | service.adapter().then(() => { 59 | expect(middleware.onRequest).toHaveBeenCalled(); 60 | expect(simplifiedSyntax.onRequest).toHaveBeenCalled(); 61 | }); 62 | }); 63 | 64 | it('runs the middlewares in order', () => { 65 | expect.assertions(1); 66 | 67 | const request = { method: 'get', param: { test: '' } }; 68 | 69 | function getMiddleware(index) { 70 | return { 71 | onRequest(config) { 72 | config.param.test += `-req${index}-`; 73 | return config; 74 | }, 75 | onResponse(resp) { 76 | resp.data.test += `-resp${index}-`; 77 | return resp; 78 | }, 79 | }; 80 | } 81 | 82 | service.register([getMiddleware(1), getMiddleware(2)]); 83 | 84 | mock.onAny().reply((config) => [200, config.param]); 85 | 86 | return http(request).then((response) => { 87 | expect(response.data.test).toBe('-req2--req1--resp1--resp2-'); 88 | }); 89 | }); 90 | 91 | it('can catch current request promise', () => { 92 | expect.assertions(1); 93 | 94 | service.register({ 95 | onSync(promise) { 96 | expect(promise).toBeInstanceOf(Promise); 97 | return promise; 98 | }, 99 | }); 100 | mock.onAny().reply(200); 101 | return http(); 102 | }); 103 | 104 | it('can return a promise for async config and response handling', () => { 105 | expect.assertions(1); 106 | 107 | const request = { method: 'get', param: { test: '' } }; 108 | 109 | function getMiddleware(index) { 110 | return { 111 | onRequest(config) { 112 | return new Promise((resolve) => { 113 | setTimeout(() => { 114 | config.param.test += `-req${index}-`; 115 | resolve(config); 116 | }, 0); 117 | }); 118 | }, 119 | onResponse(resp) { 120 | return new Promise((resolve) => { 121 | setTimeout(() => { 122 | resp.data.test += `-resp${index}-`; 123 | resolve(resp); 124 | }, 0); 125 | }); 126 | }, 127 | }; 128 | } 129 | 130 | service.register([getMiddleware(1), getMiddleware(2)]); 131 | 132 | mock.onAny().reply((config) => [200, config.param]); 133 | 134 | return http(request).then((response) => { 135 | expect(response.data.test).toBe('-req2--req1--resp1--resp2-'); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /src/service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @property {Array} middlewares stack 3 | * @property {AxiosInstance} http 4 | * @property {Function} originalAdapter 5 | */ 6 | export default class HttpMiddlewareService { 7 | /** 8 | * @param {AxiosInstance} [axios] 9 | */ 10 | constructor(axios) { 11 | this.middlewares = []; 12 | 13 | this._updateChain(); 14 | this.setHttp(axios); 15 | } 16 | 17 | /** 18 | * @param {AxiosInstance} axios 19 | * @returns {HttpMiddlewareService} 20 | */ 21 | setHttp(axios) { 22 | this.unsetHttp(); 23 | 24 | if (axios) { 25 | this.http = axios; 26 | this.originalAdapter = axios.defaults.adapter; 27 | axios.defaults.adapter = (config) => this.adapter(config); 28 | } 29 | return this; 30 | } 31 | 32 | /** 33 | * @returns {HttpMiddlewareService} 34 | */ 35 | unsetHttp() { 36 | if (this.http) { 37 | this.http.defaults.adapter = this.originalAdapter; 38 | this.http = null; 39 | } 40 | return this; 41 | } 42 | 43 | /** 44 | * @param {Object|HttpMiddleware} [middleware] 45 | * @returns {boolean} true if the middleware is already registered. 46 | */ 47 | has(middleware) { 48 | return this.middlewares.indexOf(middleware) > -1; 49 | } 50 | 51 | /** 52 | * Adds a middleware or an array of middlewares to the stack. 53 | * @param {Object|HttpMiddleware|Array} [middlewares] 54 | * @returns {HttpMiddlewareService} 55 | */ 56 | register(middlewares) { 57 | // eslint-disable-next-line no-param-reassign 58 | if (!Array.isArray(middlewares)) middlewares = [middlewares]; 59 | 60 | // Test if middlewares are registered more than once. 61 | middlewares.forEach((middleware) => { 62 | if (!middleware) return; 63 | if (this.has(middleware)) { 64 | throw new Error('Middleware already registered'); 65 | } 66 | this.middlewares.push(middleware); 67 | this._addMiddleware(middleware); 68 | }); 69 | return this; 70 | } 71 | 72 | /** 73 | * Removes a middleware from the registered stack. 74 | * @param {Object|HttpMiddleware} [middleware] 75 | * @returns {HttpMiddlewareService} 76 | */ 77 | unregister(middleware) { 78 | if (middleware) { 79 | const index = this.middlewares.indexOf(middleware); 80 | if (index > -1) { 81 | this.middlewares.splice(index, 1); 82 | } 83 | this._updateChain(); 84 | } 85 | 86 | return this; 87 | } 88 | 89 | /** 90 | * Removes all the middleware from the stack. 91 | * @returns {HttpMiddlewareService} 92 | */ 93 | reset() { 94 | this.middlewares.length = 0; 95 | this._updateChain(); 96 | return this; 97 | } 98 | 99 | /** 100 | * @param config 101 | * @returns {Promise} 102 | */ 103 | adapter(config) { 104 | return this.chain.reduce( 105 | (acc, [onResolve, onError]) => acc.then(onResolve, onError), 106 | Promise.resolve(config), 107 | ); 108 | } 109 | 110 | /** 111 | * 112 | * @param {Object} middleware 113 | * @private 114 | */ 115 | _addMiddleware(middleware) { 116 | this.chain.unshift([ 117 | middleware.onRequest && ((conf) => middleware.onRequest(conf)), 118 | middleware.onRequestError && 119 | ((error) => middleware.onRequestError(error)), 120 | ]); 121 | 122 | this.chain.push([ 123 | middleware.onResponse && ((response) => middleware.onResponse(response)), 124 | middleware.onResponseError && 125 | ((error) => middleware.onResponseError(error)), 126 | ]); 127 | } 128 | 129 | /** 130 | * @private 131 | */ 132 | _updateChain() { 133 | this.chain = [ 134 | [ 135 | (conf) => this._onSync(this.originalAdapter.call(this.http, conf)), 136 | undefined, 137 | ], 138 | ]; 139 | this.middlewares.forEach((middleware) => this._addMiddleware(middleware)); 140 | } 141 | 142 | /** 143 | * @param {Promise} promise 144 | * @returns {Promise} 145 | * @private 146 | */ 147 | _onSync(promise) { 148 | return this.middlewares.reduce( 149 | (acc, middleware) => (middleware.onSync ? middleware.onSync(acc) : acc), 150 | promise, 151 | ); 152 | } 153 | } 154 | --------------------------------------------------------------------------------