├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── jest.config.js ├── lib ├── default-options.spec.ts ├── default-options.ts ├── index.ts ├── rate-limiter.decorator.spec.ts ├── rate-limiter.decorator.ts ├── rate-limiter.guard.spec.ts ├── rate-limiter.guard.ts ├── rate-limiter.interface.spec.ts ├── rate-limiter.interface.ts ├── rate-limiter.module.spec.ts └── rate-limiter.module.ts ├── package-lock.json ├── package.json ├── tsconfig.json └── tslint.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test with Jest 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [10.x, 12.x, 14.x] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npm run build 24 | - run: npm run test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vim 2 | *.swp 3 | *.*~ 4 | 5 | # dependencies 6 | /node_modules 7 | 8 | # misc 9 | npm-debug.log 10 | .DS_Store 11 | 12 | # dist 13 | /dist 14 | 15 | /coverage 16 | 17 | # Example NestJS 18 | examples/dist 19 | examples/node_modules 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | examples 3 | coverage 4 | lib 5 | .gitignore 6 | .prettierrc 7 | jest.config.js 8 | package-lock.json 9 | tsconfig.json 10 | tslint.json 11 | README.md 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "tabWidth": 4, 5 | "printWidth": 155, 6 | "useTabs": true, 7 | "semi": false, 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Onur Ozkan 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## No longer maintained 2 | 3 |

4 | 5 |

6 | 7 |

Rate Limiter Module for NestJS

8 | 9 |

10 | Code Quality 11 | NPM Version 12 | NPM Downloads 13 | License 14 | Test 15 | 16 |

17 | 18 | # Documentation Map 19 | - [Description](https://github.com/ozkanonur/nestjs-rate-limiter#description) 20 | - [Installation](https://github.com/ozkanonur/nestjs-rate-limiter#installation) 21 | - [Basic Usage](https://github.com/ozkanonur/nestjs-rate-limiter#basic-usage) 22 | - [Include Module](https://github.com/ozkanonur/nestjs-rate-limiter#include-module) 23 | - [Using Guard](https://github.com/ozkanonur/nestjs-rate-limiter#using-guard) 24 | - [With Decorator](https://github.com/ozkanonur/nestjs-rate-limiter#with-decorator) 25 | - [With All Options](https://github.com/ozkanonur/nestjs-rate-limiter#with-all-options) 26 | - [Fastify based Graphql](https://github.com/ozkanonur/nestjs-rate-limiter#fastify-based-graphql) 27 | - [Options](https://github.com/ozkanonur/nestjs-rate-limiter#options) 28 | - [for](https://github.com/ozkanonur/nestjs-rate-limiter#-for) 29 | - [type](https://github.com/ozkanonur/nestjs-rate-limiter#-type) 30 | - [keyPrefix](https://github.com/ozkanonur/nestjs-rate-limiter#-keyPrefix) 31 | - [points](https://github.com/ozkanonur/nestjs-rate-limiter#-points) 32 | - [pointsConsumed](https://github.com/ozkanonur/nestjs-rate-limiter#-pointsConsumed) 33 | - [inmemoryBlockOnConsumed](https://github.com/ozkanonur/nestjs-rate-limiter#-inmemoryBlockOnConsumed) 34 | - [duration](https://github.com/ozkanonur/nestjs-rate-limiter#-duration) 35 | - [blockDuration](https://github.com/ozkanonur/nestjs-rate-limiter#-blockDuration) 36 | - [inmemoryBlockDuration](https://github.com/ozkanonur/nestjs-rate-limiter#-inmemoryBlockDuration) 37 | - [queueEnabled](https://github.com/ozkanonur/nestjs-rate-limiter#-queueEnabled) 38 | - [whiteList](https://github.com/ozkanonur/nestjs-rate-limiter#-whiteList) 39 | - [blackList](https://github.com/ozkanonur/nestjs-rate-limiter#-blackList) 40 | - [storeClient](https://github.com/ozkanonur/nestjs-rate-limiter#-storeClient) 41 | - [insuranceLimiter](https://github.com/ozkanonur/nestjs-rate-limiter#-insuranceLimiter) 42 | - [storeType](https://github.com/ozkanonur/nestjs-rate-limiter#-storeType) 43 | - [dbName](https://github.com/ozkanonur/nestjs-rate-limiter#-dbName) 44 | - [tableName](https://github.com/ozkanonur/nestjs-rate-limiter#-tableName) 45 | - [tableCreated](https://github.com/ozkanonur/nestjs-rate-limiter#-tableCreated) 46 | - [clearExpiredByTimeout](https://github.com/ozkanonur/nestjs-rate-limiter#-clearExpiredByTimeout) 47 | - [execEvenly](https://github.com/ozkanonur/nestjs-rate-limiter#-execEvenly) 48 | - [execEvenlyMinDelayMs](https://github.com/ozkanonur/nestjs-rate-limiter#-execEvenlyMinDelayMs) 49 | - [indexKeyPrefix](https://github.com/ozkanonur/nestjs-rate-limiter#-indexKeyPrefix) 50 | - [maxQueueSize](https://github.com/ozkanonur/nestjs-rate-limiter#-maxQueueSize) 51 | - [omitResponseHeaders](https://github.com/ozkanonur/nestjs-rate-limiter#-omitResponseHeaders) 52 | - [errorMessage](https://github.com/ozkanonur/nestjs-rate-limiter#-errorMessage) 53 | - [customResponseSchema](https://github.com/ozkanonur/nestjs-rate-limiter#-customResponseSchema) 54 | - [Override Functions](https://github.com/ozkanonur/nestjs-rate-limiter#override-functions) 55 | - [Benchmarks](https://github.com/ozkanonur/nestjs-rate-limiter#benchmarks) 56 | - [TODO List](https://github.com/ozkanonur/nestjs-rate-limiter#todo) 57 | 58 | # Description 59 | 60 | `nestjs-rate-limiter` is a module which adds in configurable rate limiting for [Nest](https://github.com/nestjs/nest) applications. 61 | 62 | Under the hood it uses [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible). 63 | 64 | # Installation 65 | 66 | ```bash 67 | npm i --save nestjs-rate-limiter 68 | ``` 69 | 70 | Or if you use Yarn: 71 | 72 | ```bash 73 | yarn add nestjs-rate-limiter 74 | ``` 75 | 76 | # Requirements 77 | 78 | `nestjs-rate-limiter` is built to work with Nest 6 and newer versions. 79 | 80 | # Basic Usage 81 | 82 | ### Include Module 83 | 84 | First you need to import this module into your main application module: 85 | 86 | > app.module.ts 87 | 88 | ```ts 89 | import { RateLimiterModule } from 'nestjs-rate-limiter' 90 | 91 | @Module({ 92 | imports: [RateLimiterModule], 93 | }) 94 | export class ApplicationModule {} 95 | ``` 96 | 97 | ### Using Guard 98 | 99 | Now you need to register the guard. You can do this only on some routes: 100 | 101 | > app.controller.ts 102 | 103 | ```ts 104 | import { RateLimiterGuard } from 'nestjs-rate-limiter' 105 | 106 | @UseGuards(RateLimiterGuard) 107 | @Get('/login') 108 | public async login() { 109 | console.log('hello') 110 | } 111 | ``` 112 | 113 | Or you can choose to register the guard globally: 114 | 115 | > app.module.ts 116 | 117 | ```ts 118 | import { APP_GUARD } from '@nestjs/core' 119 | import { RateLimiterModule, RateLimiterGuard } from 'nestjs-rate-limiter' 120 | 121 | @Module({ 122 | imports: [RateLimiterModule], 123 | providers: [ 124 | { 125 | provide: APP_GUARD, 126 | useClass: RateLimiterGuard, 127 | }, 128 | ], 129 | }) 130 | export class ApplicationModule {} 131 | ``` 132 | 133 | ### With Decorator 134 | 135 | You can use the `@RateLimit` decorator to specify the points and duration for rate limiting on a per controller or per 136 | route basis: 137 | 138 | > app.controller.ts 139 | 140 | ```ts 141 | import { RateLimit } from 'nestjs-rate-limiter' 142 | 143 | @RateLimit({ keyPrefix: 'sign-up', points: 1, duration: 60, errorMessage: 'Accounts cannot be created more than once in per minute' }) 144 | @Get('/signup') 145 | public async signUp() { 146 | console.log('hello') 147 | } 148 | ``` 149 | 150 | ### Dynamic Keyprefix 151 | 152 | ```ts 153 | import { RateLimit } from 'nestjs-rate-limiter' 154 | 155 | @RateLimit({ 156 | keyPrefix: () => programmaticFuncThatReturnsValue(), 157 | points: 1, 158 | duration: 60, 159 | customResponseSchema: () => { return { timestamp: '1611479696', message: 'Request has been blocked' }} 160 | }) 161 | @Get('/example') 162 | public async example() { 163 | console.log('hello') 164 | } 165 | ``` 166 | 167 | ### With All Options 168 | 169 | The usage of the limiter options is as in the code block below. For an explanation of the each option, please see [options](https://github.com/ozkanonur/nestjs-rate-limiter#options). 170 | 171 | ```ts 172 | @Module({ 173 | imports: [ 174 | // All the values here are defaults. 175 | RateLimiterModule.register({ 176 | for: 'Express', 177 | type: 'Memory', 178 | keyPrefix: 'global', 179 | points: 4, 180 | pointsConsumed: 1, 181 | inmemoryBlockOnConsumed: 0, 182 | duration: 1, 183 | blockDuration: 0, 184 | inmemoryBlockDuration: 0, 185 | queueEnabled: false, 186 | whiteList: [], 187 | blackList: [], 188 | storeClient: undefined, 189 | insuranceLimiter: undefined, 190 | storeType: undefined, 191 | dbName: undefined, 192 | tableName: undefined, 193 | tableCreated: undefined, 194 | clearExpiredByTimeout: undefined, 195 | execEvenly: false, 196 | execEvenlyMinDelayMs: undefined, 197 | indexKeyPrefix: {}, 198 | maxQueueSize: 100, 199 | omitResponseHeaders: false, 200 | errorMessage: 'Rate limit exceeded', 201 | logger: true, 202 | customResponseSchema: undefined 203 | }), 204 | ], 205 | providers: [ 206 | { 207 | provide: APP_GUARD, 208 | useClass: RateLimiterGuard, 209 | }, 210 | ], 211 | }) 212 | export class ApplicationModule {} 213 | ``` 214 | 215 | ### Fastify based Graphql 216 | If you want to use this library on a fastify based graphql server, you need to override the graphql context in the app.module as shown below. 217 | ```ts 218 | GraphQLModule.forRoot({ 219 | context: ({ request, reply }) => { 220 | return { req: request, res: reply } 221 | }, 222 | }), 223 | ``` 224 | 225 | # Options 226 | 227 | #### ● for 228 | Default: 'Express' 229 |
230 | Type: 'Express' | 'Fastify' | 'Microservice' | 'ExpressGraphql' | 'FastifyGraphql' 231 |
232 | 233 | In this option, you specify what the technology is running under the Nest application. The wrong value causes to limiter not working. 234 | 235 | #### ● type 236 | Default: 'Memory' 237 |
238 | Type: 'Memory' | 'Redis' | 'Memcache' | 'Postgres' | 'MySQL' | 'Mongo' 239 |
240 | 241 | Here you define where the limiter data will be stored. Each option plays a different role in limiter performance, to see that please check [benchmarks](https://github.com/ozkanonur/nestjs-rate-limiter#benchmarks). 242 | 243 | #### ● keyPrefix 244 | Default: 'global' 245 |
246 | Type: string 247 |
248 | 249 | For creating several limiters with different options to apply different modules/endpoints. 250 | 251 | Set to empty string '', if keys should be stored without prefix. 252 | 253 | Note: for some limiters it should correspond to Storage requirements for tables or collections name, as keyPrefix may be used as their name. 254 | 255 | #### ● points 256 | Default: 4 257 |
258 | Type: number 259 |
260 | 261 | Maximum number of points can be consumed over duration. 262 | 263 | #### ● pointsConsumed 264 | Default: 1 265 |
266 | Type: number 267 |
268 | 269 | You can consume more than 1 point per invocation of the rate limiter. 270 | 271 | For instance if you have a limit of 100 points per 60 seconds, and pointsConsumed is set to 10, the user will effectively be able to make 10 requests per 60 seconds. 272 | 273 | #### ● inmemoryBlockOnConsumed 274 | Default: 0 275 |
276 | Type: number 277 |
278 | 279 | For Redis, Memcached, MongoDB, MySQL, PostgreSQL, etc. 280 | 281 | Can be used against DDoS attacks. In-memory blocking works in current process memory and for consume method only. 282 | 283 | It blocks a key in memory for msBeforeNext milliseconds from the last consume result, if inmemoryBlockDuration is not set. This helps to avoid extra requests. 284 | It is not necessary to increment counter on store, if all points are consumed already. 285 | 286 | #### ● duration 287 | Default: 1 288 |
289 | Type: number 290 |
291 | 292 | Number of seconds before consumed points are reset. 293 | 294 | Keys never expire, if duration is 0. 295 | 296 | #### ● blockDuration 297 | Default: 0 298 |
299 | Type: number 300 |
301 | 302 | If positive number and consumed more than points in current duration, block for blockDuration seconds. 303 | 304 | #### ● inmemoryBlockDuration 305 | Default: 0 306 |
307 | Type: number 308 |
309 | 310 | For Redis, Memcached, MongoDB, MySQL, PostgreSQL, etc. 311 | 312 | Block key for inmemoryBlockDuration seconds, if inmemoryBlockOnConsumed or more points are consumed. Set it the same as blockDuration option for distributed application to have consistent result on all processes. 313 | 314 | #### ● queueEnabled 315 | Default: false 316 |
317 | Type: boolean 318 |
319 | 320 | It activates the queue mechanism, and holds the incoming requests for duration value. 321 | 322 | #### ● whiteList 323 | Default: [] 324 |
325 | Type: string[] 326 |
327 | 328 | If the IP is white listed, consume resolved no matter how many points consumed. 329 | 330 | #### ● blackList 331 | Default: [] 332 |
333 | Type: string[] 334 |
335 | 336 | If the IP is black listed, consume rejected anytime. Blacklisted IPs are blocked on code level not in store/memory. Think of it as of requests filter. 337 | 338 | #### ● storeClient 339 | Default: undefined 340 |
341 | Type: any 342 |
343 | 344 | Required for Redis, Memcached, MongoDB, MySQL, PostgreSQL, etc. 345 | 346 | Have to be redis, ioredis, memcached, mongodb, pg, mysql2, mysql or any other related pool or connection. 347 | 348 | #### ● insuranceLimiter 349 | Default: undefined 350 |
351 | Type: any 352 |
353 | 354 | Default: undefined For Redis, Memcached, MongoDB, MySQL, PostgreSQL. 355 | 356 | Instance of RateLimiterAbstract extended object to store limits, when database comes up with any error. 357 | 358 | All data from insuranceLimiter is NOT copied to parent limiter, when error gone 359 | 360 | Note: insuranceLimiter automatically setup blockDuration and execEvenly to same values as in parent to avoid unexpected behaviour. 361 | 362 | #### ● storeType 363 | Default: storeClient.constructor.name 364 |
365 | Type: any 366 |
367 | 368 | For MySQL and PostgreSQL 369 | It is required only for Knex and have to be set to 'knex' 370 | 371 | #### ● dbName 372 | Default for MySQL, Postgres & Mongo: 'rate-limiter' 373 |
374 | Type: string 375 |
376 | 377 | Database where limits are stored. It is created during creating a limiter. Doesn't work with Mongoose, as mongoose connection is established to exact database. 378 | 379 | #### ● tableName 380 | Default: equals to 'keyPrefix' option 381 |
382 | Type: string 383 |
384 | 385 | For MongoDB, MySQL, PostgreSQL. 386 | 387 | By default, limiter creates a table for each unique keyPrefix. tableName option sets table/collection name where values should be store. 388 | 389 | #### ● tableCreated 390 | Default: false 391 |
392 | Type: boolean 393 |
394 | 395 | Does not create a table for rate limiter, if tableCreated is true. 396 | 397 | #### ● clearExpiredByTimeout 398 | Default for MySQL and PostgreSQL: true 399 |
400 | Type: boolean 401 |
402 | 403 | Rate limiter deletes data expired more than 1 hour ago every 5 minutes. 404 | 405 | #### ● execEvenly 406 | Default: false 407 |
408 | Type: boolean 409 |
410 | 411 | Delay action to be executed evenly over duration First action in duration is executed without delay. All next allowed actions in current duration are delayed by formula msBeforeDurationEnd / (remainingPoints + 2) with minimum delay of duration * 1000 / points It allows to cut off load peaks similar way to Leaky Bucket. 412 | 413 | Note: it isn't recommended to use it for long duration and few points, as it may delay action for too long with default execEvenlyMinDelayMs. 414 | 415 | #### ● execEvenlyMinDelayMs 416 | Default: duration * 1000 / points 417 |
418 | Type: number 419 |
420 | 421 | Sets minimum delay in milliseconds, when action is delayed with execEvenly 422 | 423 | #### ● indexKeyPrefix 424 | Default: {} 425 |
426 | Type: {} 427 |
428 | 429 | Object which is used to create combined index by {...indexKeyPrefix, key: 1} attributes. 430 | 431 | #### ● maxQueueSize 432 | Default: 100 433 |
434 | Type: number 435 |
436 | 437 | Determines the maximum number of requests in the queue and returns 429 as response to requests that over of the maxQueueSize. 438 | 439 | #### ● omitResponseHeaders 440 | Default: false 441 |
442 | Type: boolean 443 |
444 | 445 | Whether or not the rate limit headers (X-Retry-After, X-RateLimit-Limit, X-Retry-Remaining, X-Retry-Reset) should be omitted in the response. 446 | 447 | 448 | #### ● errorMessage 449 | Default: 'Rate limit exceeded' 450 |
451 | Type: string 452 |
453 | 454 | errorMessage option can change the error message of rate limiter exception. 455 | 456 | #### ● logger 457 | Default: true 458 |
459 | Type: boolean 460 |
461 | 462 | logger option allows to enable or disable logging from library. 463 | 464 | #### ● customResponseSchema 465 | Default: undefined 466 |
467 | Type: string 468 |
469 | 470 | customResponseSchema option allows to provide customizable response schemas 471 | 472 | # Override Functions 473 | 474 | It's possible to override getIpFromRequest function by extending RateLimiterGuard class. 475 | 476 | ```ts 477 | import { RateLimiterGuard } from 'nestjs-rate-limiter' 478 | import type { Request } from 'express' 479 | 480 | class ExampleRateLimiterGuard extends RateLimiterGuard { 481 | protected getIpFromRequest(request: Request): string { 482 | return request.get('x-forwarded-for'); 483 | } 484 | } 485 | ``` 486 | 487 | # Benchmarks 488 | 489 | 1000 concurrent clients with maximum 2000 requests per sec during 30 seconds. 490 | 491 | ``` 492 | 1. Memory 0.34 ms 493 | 2. Redis 2.45 ms 494 | 3. Memcached 3.89 ms 495 | 4. Mongo 4.75 ms 496 | ``` 497 | 498 | 500 concurrent clients with maximum 1000 req per sec during 30 seconds 499 | 500 | ``` 501 | 5. PostgreSQL 7.48 ms (with connection pool max 100) 502 | 6. MySQL 14.59 ms (with connection pool 100) 503 | ``` 504 | 505 | ## TODO 506 | - [ ] Support Websocket 507 | - [ ] Support Rpc 508 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/lib'], 3 | transform: { 4 | '^.+\\.ts?$': 'ts-jest', 5 | }, 6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts?$', 7 | moduleFileExtensions: ['ts', 'js', 'jsx', 'json', 'node'], 8 | collectCoverage: true, 9 | coverageDirectory: "./coverage", 10 | coverageThreshold: { 11 | global: { 12 | functions: 70, 13 | lines: 80, 14 | statements: 80, 15 | }, 16 | }, 17 | } -------------------------------------------------------------------------------- /lib/default-options.spec.ts: -------------------------------------------------------------------------------- 1 | import { defaultRateLimiterOptions } from './default-options' 2 | 3 | describe('defaultRateLimiterOptions', () => { 4 | it('should validate that defaultRateLimiterOptions exists', async () => { 5 | expect(defaultRateLimiterOptions).toBeDefined() 6 | }) 7 | 8 | it('should validate the defaultRateLimiterOptions', async () => { 9 | expect(defaultRateLimiterOptions.for).toBe('Express') 10 | expect(defaultRateLimiterOptions.type).toBe('Memory') 11 | expect(defaultRateLimiterOptions.keyPrefix).toBe('global') 12 | expect(defaultRateLimiterOptions.points).toBe(4) 13 | expect(defaultRateLimiterOptions.pointsConsumed).toBe(1) 14 | expect(defaultRateLimiterOptions.inmemoryBlockOnConsumed).toBe(0) 15 | expect(defaultRateLimiterOptions.duration).toBe(1) 16 | expect(defaultRateLimiterOptions.blockDuration).toBe(0) 17 | expect(defaultRateLimiterOptions.inmemoryBlockDuration).toBe(0) 18 | expect(defaultRateLimiterOptions.queueEnabled).toBe(false) 19 | expect(defaultRateLimiterOptions.whiteList.length).toBe(0) 20 | expect(defaultRateLimiterOptions.blackList.length).toBe(0) 21 | expect(defaultRateLimiterOptions.storeClient).toBeUndefined() 22 | expect(defaultRateLimiterOptions.insuranceLimiter).toBeUndefined() 23 | expect(defaultRateLimiterOptions.dbName).toBe('rate-limiter') 24 | expect(defaultRateLimiterOptions.tableName).toBeUndefined() 25 | expect(defaultRateLimiterOptions.tableCreated).toBeUndefined() 26 | expect(defaultRateLimiterOptions.clearExpiredByTimeout).toBeUndefined() 27 | expect(defaultRateLimiterOptions.execEvenly).toBe(false) 28 | expect(defaultRateLimiterOptions.execEvenlyMinDelayMs).toBeUndefined() 29 | expect(Object.keys(defaultRateLimiterOptions.indexKeyPrefix).length).toBe(0) 30 | expect(defaultRateLimiterOptions.maxQueueSize).toBe(100) 31 | expect(defaultRateLimiterOptions.omitResponseHeaders).toBe(false) 32 | expect(defaultRateLimiterOptions.errorMessage).toBe('Rate limit exceeded') 33 | expect(defaultRateLimiterOptions.customResponseSchema).toBeUndefined() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /lib/default-options.ts: -------------------------------------------------------------------------------- 1 | import { RateLimiterOptions } from './rate-limiter.interface' 2 | 3 | export const defaultRateLimiterOptions: RateLimiterOptions = { 4 | for: 'Express', 5 | type: 'Memory', 6 | keyPrefix: 'global', 7 | points: 4, 8 | pointsConsumed: 1, 9 | inmemoryBlockOnConsumed: 0, 10 | duration: 1, 11 | blockDuration: 0, 12 | inmemoryBlockDuration: 0, 13 | queueEnabled: false, 14 | whiteList: [], 15 | blackList: [], 16 | storeClient: undefined, 17 | insuranceLimiter: undefined, 18 | storeType: undefined, 19 | dbName: 'rate-limiter', 20 | tableName: undefined, 21 | tableCreated: undefined, 22 | clearExpiredByTimeout: undefined, 23 | execEvenly: false, 24 | execEvenlyMinDelayMs: undefined, 25 | indexKeyPrefix: {}, 26 | maxQueueSize: 100, 27 | omitResponseHeaders: false, 28 | errorMessage: 'Rate limit exceeded', 29 | customResponseSchema: undefined, 30 | logger: true 31 | } 32 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rate-limiter.decorator' 2 | export * from './rate-limiter.interface' 3 | export * from './rate-limiter.module' 4 | export * from './rate-limiter.guard' 5 | -------------------------------------------------------------------------------- /lib/rate-limiter.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { RateLimit } from './rate-limiter.decorator' 2 | import { RateLimiterOptions } from './rate-limiter.interface' 3 | 4 | describe('RateLimit', () => { 5 | it('should validate that RateLimit decorator exists', async () => { 6 | expect(RateLimit).toBeDefined() 7 | }) 8 | 9 | it('should verify RateLimit Method decorator can be created with empty options', async () => { 10 | const options: RateLimiterOptions = {} 11 | 12 | const decorator: MethodDecorator = RateLimit(options) 13 | 14 | expect(decorator).toBeDefined() 15 | }) 16 | 17 | it('should verify RateLimit can decorate a method and be called', async () => { 18 | const options: RateLimiterOptions = {} 19 | const testFn = jest.fn() 20 | 21 | class TestController { 22 | @RateLimit(options) 23 | run() { 24 | testFn() 25 | } 26 | } 27 | 28 | const controller = new TestController() 29 | controller.run() 30 | 31 | expect(testFn).toHaveBeenCalled() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /lib/rate-limiter.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common' 2 | import { RateLimiterOptions } from './rate-limiter.interface' 3 | 4 | export const RateLimit = (options: RateLimiterOptions): MethodDecorator => SetMetadata('rateLimit', options) 5 | -------------------------------------------------------------------------------- /lib/rate-limiter.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { RateLimit } from './rate-limiter.decorator' 2 | import { RateLimiterOptions } from './rate-limiter.interface' 3 | 4 | describe('RateLimit', () => { 5 | it('should validate that RateLimit decorator exists', async () => { 6 | expect(RateLimit).toBeDefined() 7 | }) 8 | 9 | it('should verify RateLimit Method decorator can be created with empty options', async () => { 10 | const options: RateLimiterOptions = {} 11 | 12 | const decorator: MethodDecorator = RateLimit(options) 13 | 14 | expect(decorator).toBeDefined() 15 | }) 16 | 17 | it('should verify RateLimit can decorate a method and be called', async () => { 18 | const options: RateLimiterOptions = {} 19 | const testFn = jest.fn() 20 | 21 | class TestController { 22 | @RateLimit(options) 23 | run() { 24 | testFn() 25 | } 26 | } 27 | 28 | const controller = new TestController() 29 | controller.run() 30 | 31 | expect(testFn).toHaveBeenCalled() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /lib/rate-limiter.guard.ts: -------------------------------------------------------------------------------- 1 | import { Reflector } from '@nestjs/core' 2 | import { Injectable, ExecutionContext, Inject, HttpStatus, Logger, HttpException, CanActivate } from '@nestjs/common' 3 | import { 4 | RateLimiterMemory, 5 | RateLimiterRes, 6 | RateLimiterAbstract, 7 | RateLimiterRedis, 8 | IRateLimiterStoreOptions, 9 | RateLimiterMemcache, 10 | RateLimiterPostgres, 11 | RateLimiterMySQL, 12 | RateLimiterMongo, 13 | RateLimiterQueue, 14 | RLWrapperBlackAndWhite 15 | } from 'rate-limiter-flexible' 16 | import { RateLimiterOptions } from './rate-limiter.interface' 17 | import { defaultRateLimiterOptions } from './default-options' 18 | 19 | @Injectable() 20 | export class RateLimiterGuard implements CanActivate { 21 | private rateLimiters: Map = new Map() 22 | private specificOptions: RateLimiterOptions 23 | private queueLimiter: RateLimiterQueue 24 | 25 | constructor(@Inject('RATE_LIMITER_OPTIONS') private options: RateLimiterOptions, @Inject('Reflector') private readonly reflector: Reflector) {} 26 | 27 | async getRateLimiter(options?: RateLimiterOptions): Promise { 28 | this.options = { ...defaultRateLimiterOptions, ...this.options } 29 | this.specificOptions = null 30 | this.specificOptions = options 31 | 32 | const limiterOptions: RateLimiterOptions = { 33 | ...this.options, 34 | ...options 35 | } 36 | 37 | const { ...libraryArguments } = limiterOptions 38 | 39 | let rateLimiter: RateLimiterAbstract = this.rateLimiters.get(libraryArguments.keyPrefix) 40 | 41 | if (libraryArguments.execEvenlyMinDelayMs === undefined) 42 | libraryArguments.execEvenlyMinDelayMs = (this.options.duration * 1000) / this.options.points 43 | 44 | if (!rateLimiter) { 45 | const logger = this.specificOptions?.logger || this.options.logger 46 | switch (this.specificOptions?.type || this.options.type) { 47 | case 'Memory': 48 | rateLimiter = new RateLimiterMemory(libraryArguments) 49 | if (logger) { 50 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterMemory') 51 | } 52 | break 53 | case 'Redis': 54 | rateLimiter = new RateLimiterRedis(libraryArguments as IRateLimiterStoreOptions) 55 | if (logger) { 56 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterRedis') 57 | } 58 | break 59 | case 'Memcache': 60 | rateLimiter = new RateLimiterMemcache(libraryArguments as IRateLimiterStoreOptions) 61 | if (logger) { 62 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterMemcache') 63 | } 64 | break 65 | case 'Postgres': 66 | if (libraryArguments.storeType === undefined) libraryArguments.storeType = this.options.storeClient.constructor.name 67 | 68 | libraryArguments.tableName = this.specificOptions?.tableName || this.options.tableName 69 | if (libraryArguments.tableName === undefined) { 70 | libraryArguments.tableName = this.specificOptions?.keyPrefix || this.options.keyPrefix 71 | } 72 | 73 | if (libraryArguments.tableCreated === undefined) libraryArguments.tableCreated = false 74 | if (libraryArguments.clearExpiredByTimeout === undefined) libraryArguments.clearExpiredByTimeout = true 75 | 76 | rateLimiter = await new Promise((resolve, reject) => { 77 | const limiter = new RateLimiterPostgres(libraryArguments as IRateLimiterStoreOptions, (err) => { 78 | if (err) { 79 | reject(err) 80 | } else { 81 | resolve(limiter) 82 | } 83 | }) 84 | }) 85 | if (logger) { 86 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterPostgres') 87 | } 88 | break 89 | case 'MySQL': 90 | if (libraryArguments.storeType === undefined) libraryArguments.storeType = this.options.storeClient.constructor.name 91 | 92 | libraryArguments.tableName = this.specificOptions?.tableName || this.options.tableName 93 | if (libraryArguments.tableName === undefined) { 94 | libraryArguments.tableName = this.specificOptions?.keyPrefix || this.options.keyPrefix 95 | } 96 | 97 | if (libraryArguments.tableCreated === undefined) libraryArguments.tableCreated = false 98 | if (libraryArguments.clearExpiredByTimeout === undefined) libraryArguments.clearExpiredByTimeout = true 99 | 100 | rateLimiter = await new Promise((resolve, reject) => { 101 | const limiter = new RateLimiterMySQL(libraryArguments as IRateLimiterStoreOptions, (err) => { 102 | if (err) { 103 | reject(err) 104 | } else { 105 | resolve(limiter) 106 | } 107 | }) 108 | }) 109 | if (logger) { 110 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterMySQL') 111 | } 112 | break 113 | case 'Mongo': 114 | if (libraryArguments.storeType === undefined) libraryArguments.storeType = this.options.storeClient.constructor.name 115 | 116 | libraryArguments.tableName = this.specificOptions?.tableName || this.options.tableName 117 | if (libraryArguments.tableName === undefined) { 118 | libraryArguments.tableName = this.specificOptions?.keyPrefix || this.options.keyPrefix 119 | } 120 | 121 | rateLimiter = new RateLimiterMongo(libraryArguments as IRateLimiterStoreOptions) 122 | if (logger) { 123 | Logger.log(`Rate Limiter started with ${limiterOptions.keyPrefix} key prefix`, 'RateLimiterMongo') 124 | } 125 | break 126 | default: 127 | throw new Error(`Invalid "type" option provided to RateLimiterGuard. Value was ${limiterOptions.type}`) 128 | } 129 | 130 | this.rateLimiters.set(limiterOptions.keyPrefix, rateLimiter) 131 | } 132 | 133 | if (this.specificOptions?.queueEnabled || this.options.queueEnabled) { 134 | this.queueLimiter = new RateLimiterQueue(rateLimiter, { 135 | maxQueueSize: this.specificOptions?.maxQueueSize || this.options.maxQueueSize 136 | }) 137 | } 138 | 139 | rateLimiter = new RLWrapperBlackAndWhite({ 140 | limiter: rateLimiter, 141 | whiteList: this.specificOptions?.whiteList || this.options.whiteList, 142 | blackList: this.specificOptions?.blackList || this.options.blackList, 143 | runActionAnyway: false 144 | }) 145 | 146 | return rateLimiter 147 | } 148 | 149 | async canActivate(context: ExecutionContext): Promise { 150 | let points: number = this.specificOptions?.points || this.options.points 151 | let pointsConsumed: number = this.specificOptions?.pointsConsumed || this.options.pointsConsumed 152 | 153 | const reflectedOptions: RateLimiterOptions = this.reflector.get('rateLimit', context.getHandler()) 154 | 155 | if (reflectedOptions) { 156 | if (reflectedOptions.points) { 157 | points = reflectedOptions.points 158 | } 159 | 160 | if (reflectedOptions.pointsConsumed) { 161 | pointsConsumed = reflectedOptions.pointsConsumed 162 | } 163 | } 164 | 165 | const request = this.httpHandler(context).req 166 | const response = this.httpHandler(context).res 167 | 168 | const rateLimiter: RateLimiterAbstract = await this.getRateLimiter(reflectedOptions) 169 | const key = this.getIpFromRequest(request) 170 | 171 | await this.responseHandler(response, key, rateLimiter, points, pointsConsumed) 172 | return true 173 | } 174 | 175 | protected getIpFromRequest(request: { ip: string }): string { 176 | return request.ip?.match(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/)?.[0] 177 | } 178 | 179 | private httpHandler(context: ExecutionContext) { 180 | if (this.options.for === 'ExpressGraphql') { 181 | return { 182 | req: context.getArgByIndex(2).req, 183 | res: context.getArgByIndex(2).req.res 184 | } 185 | } else if (this.options.for === 'FastifyGraphql') { 186 | return { 187 | req: context.getArgByIndex(2).req, 188 | res: context.getArgByIndex(2).res 189 | } 190 | } else { 191 | return { 192 | req: context.switchToHttp().getRequest(), 193 | res: context.switchToHttp().getResponse() 194 | } 195 | } 196 | } 197 | 198 | private async setResponseHeaders(response: any, points: number, rateLimiterResponse: RateLimiterRes) { 199 | response.header('Retry-After', Math.ceil(rateLimiterResponse.msBeforeNext / 1000)) 200 | response.header('X-RateLimit-Limit', points) 201 | response.header('X-Retry-Remaining', rateLimiterResponse.remainingPoints) 202 | response.header('X-Retry-Reset', new Date(Date.now() + rateLimiterResponse.msBeforeNext).toUTCString()) 203 | } 204 | 205 | private async responseHandler(response: any, key: any, rateLimiter: RateLimiterAbstract, points: number, pointsConsumed: number) { 206 | try { 207 | if (this.specificOptions?.queueEnabled || this.options.queueEnabled) await this.queueLimiter.removeTokens(1) 208 | else { 209 | const rateLimiterResponse: RateLimiterRes = await rateLimiter.consume(key, pointsConsumed) 210 | if (!this.specificOptions?.omitResponseHeaders && !this.options.omitResponseHeaders) 211 | this.setResponseHeaders(response, points, rateLimiterResponse) 212 | } 213 | } catch (rateLimiterResponse) { 214 | response.header('Retry-After', Math.ceil(rateLimiterResponse.msBeforeNext / 1000)) 215 | if (typeof this.specificOptions?.customResponseSchema === 'function' || typeof this.options.customResponseSchema === 'function') { 216 | const errorBody = this.specificOptions?.customResponseSchema || this.options.customResponseSchema 217 | throw new HttpException(errorBody(rateLimiterResponse), HttpStatus.TOO_MANY_REQUESTS) 218 | } else { 219 | throw new HttpException(this.specificOptions?.errorMessage || this.options.errorMessage, HttpStatus.TOO_MANY_REQUESTS) 220 | } 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /lib/rate-limiter.interface.spec.ts: -------------------------------------------------------------------------------- 1 | import { RateLimiterOptionsFactory, RateLimiterModuleAsyncOptions, RateLimiterOptions } from './rate-limiter.interface' 2 | 3 | describe('RateLimiterOptionsFactory', () => { 4 | it('should validate that RateLimiterOptionsFactory exists', async () => { 5 | const rateLimiterOptionsFactory: RateLimiterOptionsFactory = { 6 | createRateLimiterOptions: jest.fn() 7 | } 8 | expect(rateLimiterOptionsFactory).toBeDefined() 9 | expect(rateLimiterOptionsFactory.createRateLimiterOptions).toBeDefined() 10 | }) 11 | }) 12 | 13 | describe('RateLimiterModuleAsyncOptions', () => { 14 | it('should validate that RateLimiterModuleAsyncOptions with no properties', async () => { 15 | const rateLimiterModuleAsyncOptions: RateLimiterModuleAsyncOptions = {} 16 | expect(rateLimiterModuleAsyncOptions).toBeDefined() 17 | expect(rateLimiterModuleAsyncOptions.useExisting).toBeUndefined() 18 | }) 19 | 20 | it('should validate that RateLimiterModuleAsyncOptions with optional properties', async () => { 21 | const rateLimiterModuleAsyncOptions: RateLimiterModuleAsyncOptions = { 22 | useFactory: jest.fn(), 23 | inject: ['test'] 24 | } 25 | expect(rateLimiterModuleAsyncOptions).toBeDefined() 26 | expect(rateLimiterModuleAsyncOptions.useFactory).toBeDefined() 27 | expect(rateLimiterModuleAsyncOptions.inject.length).toBe(1) 28 | }) 29 | }) 30 | 31 | describe('RateLimiterOptions', () => { 32 | it('should validate that RateLimiterOptions with no properties', async () => { 33 | const rateLimiterOptions: RateLimiterOptions = {} 34 | expect(rateLimiterOptions).toBeDefined() 35 | expect(rateLimiterOptions.for).toBeUndefined() 36 | }) 37 | 38 | it('should validate that RateLimiterOptions with no properties', async () => { 39 | const rateLimiterOptions: RateLimiterOptions = { 40 | for: 'Express', 41 | type: 'Memory', 42 | points: 2, 43 | pointsConsumed: 3, 44 | dbName: 'test' 45 | } 46 | expect(rateLimiterOptions).toBeDefined() 47 | expect(rateLimiterOptions.for).toBe('Express') 48 | expect(rateLimiterOptions.type).toBe('Memory') 49 | expect(rateLimiterOptions.points).toBe(2) 50 | expect(rateLimiterOptions.pointsConsumed).toBe(3) 51 | expect(rateLimiterOptions.dbName).toBe('test') 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /lib/rate-limiter.interface.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common' 2 | import { ModuleMetadata, Type } from '@nestjs/common/interfaces' 3 | import { RateLimiterRes } from 'rate-limiter-flexible' 4 | 5 | export interface RateLimiterOptions { 6 | for?: 'Express' | 'Fastify' | 'Microservice' | 'ExpressGraphql' | 'FastifyGraphql' 7 | type?: 'Memory' | 'Redis' | 'Memcache' | 'Postgres' | 'MySQL' | 'Mongo' 8 | keyPrefix?: string 9 | points?: number 10 | pointsConsumed?: number 11 | inmemoryBlockDuration?: number 12 | duration?: number 13 | blockDuration?: number 14 | inmemoryBlockOnConsumed?: number 15 | queueEnabled?: boolean 16 | whiteList?: string[] 17 | blackList?: string[] 18 | storeClient?: any 19 | insuranceLimiter?: any 20 | storeType?: string 21 | dbName?: string 22 | tableName?: string 23 | tableCreated?: boolean 24 | clearExpiredByTimeout?: boolean 25 | execEvenly?: boolean 26 | execEvenlyMinDelayMs?: number 27 | indexKeyPrefix?: {} 28 | maxQueueSize?: number 29 | omitResponseHeaders?: boolean 30 | errorMessage?: string 31 | logger?: boolean 32 | customResponseSchema?: (rateLimiterResponse: RateLimiterRes) => {} 33 | } 34 | 35 | export interface RateLimiterOptionsFactory { 36 | createRateLimiterOptions(): Promise | RateLimiterOptions 37 | } 38 | 39 | export interface RateLimiterModuleAsyncOptions extends Pick { 40 | useExisting?: Type 41 | useClass?: Type 42 | useFactory?: (...args: any[]) => Promise | RateLimiterOptions 43 | inject?: any[] 44 | extraProviders?: Provider[] 45 | } 46 | -------------------------------------------------------------------------------- /lib/rate-limiter.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Provider } from '@nestjs/common' 2 | import { RateLimiterModule } from './rate-limiter.module' 3 | import { RateLimiterOptions, RateLimiterModuleAsyncOptions } from './rate-limiter.interface' 4 | 5 | describe('RateLimiterModule', () => { 6 | it('should validate that RateLimiterModule exists', async () => { 7 | expect(RateLimiterModule).toBeDefined() 8 | }) 9 | 10 | it('should register RateLimiterModule with empty options', async () => { 11 | const rateLimiterOptions: RateLimiterOptions = {} 12 | 13 | const registeredDynamicModule: DynamicModule = RateLimiterModule.register(rateLimiterOptions) 14 | 15 | expect(registeredDynamicModule).toBeDefined() 16 | expect(typeof registeredDynamicModule.module).toBeDefined() 17 | expect(registeredDynamicModule.providers.length).toBe(1) 18 | const rateLimitOptionsProvider: any = registeredDynamicModule.providers[0] 19 | expect(rateLimitOptionsProvider.provide).toBe('RATE_LIMITER_OPTIONS') 20 | expect(rateLimitOptionsProvider.useValue).toBeDefined() 21 | }) 22 | 23 | it('should register RateLimiterModule with default options', async () => { 24 | const rateLimiterOptions: RateLimiterOptions = {} 25 | 26 | const registeredDynamicModule: DynamicModule = RateLimiterModule.register() 27 | 28 | expect(registeredDynamicModule).toBeDefined() 29 | expect(typeof registeredDynamicModule.module).toBeDefined() 30 | expect(registeredDynamicModule.providers.length).toBe(1) 31 | const rateLimitOptionsProvider: any = registeredDynamicModule.providers[0] 32 | expect(rateLimitOptionsProvider.provide).toBe('RATE_LIMITER_OPTIONS') 33 | expect(rateLimitOptionsProvider.useValue).toBeDefined() 34 | 35 | const options: RateLimiterOptions = rateLimitOptionsProvider.useValue 36 | 37 | expect(options.for).toBe('Express') 38 | }) 39 | 40 | it('should register async RateLimiterModule with async options', async () => { 41 | const rateLimiterOptions: RateLimiterModuleAsyncOptions = {} 42 | 43 | const registeredDynamicModule: DynamicModule = RateLimiterModule.registerAsync(rateLimiterOptions) 44 | 45 | expect(registeredDynamicModule).toBeDefined() 46 | expect(typeof registeredDynamicModule.module).toBeDefined() 47 | expect(registeredDynamicModule.providers.length).toBe(2) 48 | const rateLimitOptionsProvider: any = registeredDynamicModule.providers[0] 49 | expect(rateLimitOptionsProvider.provide).toBe('RATE_LIMITER_OPTIONS') 50 | expect(rateLimitOptionsProvider.useFactory).toBeDefined() 51 | expect(rateLimitOptionsProvider.inject).toBeDefined() 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /lib/rate-limiter.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, DynamicModule, Provider } from '@nestjs/common' 2 | import { defaultRateLimiterOptions } from './default-options' 3 | import { RateLimiterOptions, RateLimiterModuleAsyncOptions, RateLimiterOptionsFactory } from './rate-limiter.interface' 4 | 5 | @Module({ 6 | exports: ['RATE_LIMITER_OPTIONS'], 7 | providers: [{ provide: 'RATE_LIMITER_OPTIONS', useValue: defaultRateLimiterOptions }] 8 | }) 9 | export class RateLimiterModule { 10 | static register(options: RateLimiterOptions = defaultRateLimiterOptions): DynamicModule { 11 | return { 12 | module: RateLimiterModule, 13 | providers: [{ provide: 'RATE_LIMITER_OPTIONS', useValue: options }] 14 | } 15 | } 16 | 17 | static registerAsync(options: RateLimiterModuleAsyncOptions): DynamicModule { 18 | return { 19 | module: RateLimiterModule, 20 | imports: options.imports, 21 | providers: [...this.createAsyncProviders(options), ...(options.extraProviders || [])] 22 | } 23 | } 24 | 25 | private static createAsyncProviders(options: RateLimiterModuleAsyncOptions): Provider[] { 26 | if (options.useExisting || options.useFactory) { 27 | return [this.createAsyncOptionsProvider(options)] 28 | } 29 | return [ 30 | this.createAsyncOptionsProvider(options), 31 | { 32 | provide: options.useClass, 33 | useClass: options.useClass 34 | } 35 | ] 36 | } 37 | 38 | private static createAsyncOptionsProvider(options: RateLimiterModuleAsyncOptions): Provider { 39 | if (options.useFactory) { 40 | return { 41 | provide: 'RATE_LIMITER_OPTIONS', 42 | useFactory: options.useFactory, 43 | inject: options.inject || [] 44 | } 45 | } 46 | return { 47 | provide: 'RATE_LIMITER_OPTIONS', 48 | useFactory: async (optionsFactory: RateLimiterOptionsFactory) => optionsFactory.createRateLimiterOptions(), 49 | inject: [options.useExisting || options.useClass] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-rate-limiter", 3 | "version": "3.1.0", 4 | "description": "Highly configurable and extensible rate limiter library", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/ozkanonur/nestjs-rate-limiter.git" 8 | }, 9 | "keywords": [ 10 | "nestjs", 11 | "nest", 12 | "rate-limiter", 13 | "request-limiter", 14 | "security" 15 | ], 16 | "main": "dist/index.js", 17 | "author": "Onur Ozkan ", 18 | "contributors": [ 19 | "Onur Ozkan ", 20 | "Ryan Dowling " 21 | ], 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/ozkanonur/nestjs-rate-limiter/issues" 25 | }, 26 | "homepage": "https://github.com/ozkanonur/nestjs-rate-limiter#readme", 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "scripts": { 31 | "build": "rm -rf dist && tsc -p tsconfig.json", 32 | "lint": "prettier --write lib", 33 | "test": "jest --config ./jest.config.js" 34 | }, 35 | "dependencies": { 36 | "rate-limiter-flexible": "2.1.10" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/common": "latest", 40 | "@nestjs/core": "latest", 41 | "@types/jest": "^26.0.15", 42 | "@types/node": "^14.11.1", 43 | "jest": "^26.6.3", 44 | "prettier": "^2.1.1", 45 | "reflect-metadata": "0.1.13", 46 | "rxjs": "^6.6.3", 47 | "ts-node": "^9.0.0", 48 | "ts-jest": "^26.4.4", 49 | "typescript": "^4.0.2" 50 | } 51 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es6", 11 | "sourceMap": false, 12 | "outDir": "./dist", 13 | "rootDir": "./lib", 14 | "skipLibCheck": true 15 | }, 16 | "include": [ 17 | "lib/**/*" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "quotemark": [ 11 | true, 12 | "single" 13 | ], 14 | "member-access": [ 15 | false 16 | ], 17 | "ordered-imports": [ 18 | false 19 | ], 20 | "max-line-length": [ 21 | true, 22 | 155 23 | ], 24 | "member-ordering": [ 25 | false 26 | ], 27 | "interface-name": [ 28 | false 29 | ], 30 | "arrow-parens": false, 31 | "object-literal-sort-keys": false 32 | }, 33 | "rulesDirectory": [] 34 | } 35 | --------------------------------------------------------------------------------