├── 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 | - [](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 | [](https://travis-ci.org/emileber/axios-middleware)
4 | [](https://www.npmjs.com/package/axios-middleware)
5 | [](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 |
--------------------------------------------------------------------------------