How to rate-limit your Node/Express API (the easy way)

Whether you're starting a SaaS company or piecing together a small side project, at some point you should think about rate-limiting your Node/Express API.

What is a rate limit?

"A rate limit is the number of API calls an app or user can make within a given time period." - Facebook

Why include rate-limiting in your API?

If you don't rate-limit your API, you're basically allowing any user to make unlimited requests. This can cause serious resource issues, not to mention increasing the likelihood of getting a large bill from your hosting provider.

By adding rate-limiting you are protecting yourself from all kinds of problems-including a whole category of cyber attacks- and making your API more scalable.

Alright, we get it. Now how do I rate-limit my Express API?

As of right now, the most trusted module for this task is a simple one called express-rate-limit.

If you take a look at the repo you'll see a few useful examples for getting started. The basic idea is to run:

npm install --save express-rate-limit

and then bring it into your application to help you create some basic middleware, like so:

Without having to do too much heavy lifting, you can rest assured that line 11- which tells your API to use the limiter middleware we've created on line 6- will cause the rate limit to be applied to all routes in the server.js file above, no matter what.

And in this case, that limit would be 100 requests per user, per 15 minutes. The windowMs property lets you set the window of time, in milliseconds, that your limit is based on, while the max property allows you to set the max amount of requests during that window.

But of course you can change those numbersto be whatever you want them to be.

Congrats! We've just made our application that much safer, and that much more scalable, with only 5 lines of code!

Custom limiting different routes

But what if you want to have different limits for different routes?

For example, maybe you want to be even stingier with your computational resources, and realize that a login attempt should really only be allowed to happen 10 times per minute, for both security reasons and resource-saving concerns.

Once logged in, however, you realize that a user ought to be able to make far more requests. For example, they should be able to create, edit, and delete todo items from a Todo list without worrying too much that they'll be blocked by your API. You decide that 200 requests per 5 minutes will protect you from any craziness, while still allowing your user the space and freedom they need to do their thing.

In this scenario, it would work to do something like this:

In the above example, we've create 2 separate middleware- loginLimiter() and addItemLimiter()- using the same rateLimit() function as before. But now we're only passing them in as middleware arguments to the routes that need them.

This is all well and good, but isn't this kind of clunky? I mean what if we have an actual business app, with say, 30 different routes available in it? Assuming I want somewhat granular control over my rate limits, that's going to be a lot of extra lines of code for all that middleware!

You raise a very good point. Let me show you what I like to do...

Open up the limit

Yes, we are now quoting 80s movie theme songs.

We've made it this far. I think we've earned it.

Anyway, the way I see it there are 2 tried-and-true ways that you can immediately begin cleaning up any entry point (server.js) file in Node and Express:

1) Start using express.Router()

2) Put module.exports to work for you

I won't focus too much on express.Router right now because there are plenty of great guides out there on how to start using it (including the express docs themselves) and because I want to remain focused on rate-limiting.

So I guess that means we're just going to jump right to point #2.

For this next example, I'm going to show you 2 images. The first image shows us creating a new file (ratelimits.js) and then using module.exports to export our work from it. The second image shows our server.js file again, and how we can access our exported methods from ratelimit.js very easily by just requiring it:

(note that we are using the same two routes as the previous example)

Although things got a little bit fancier, all we really did was take the same functions we made before and attach them as methods to an object inside of ratelimits.js, and import them back in.

It was only 2 routes but we managed to clear over 10 lines of code!

Did you notice that I actually snuck in a third method too? It's the one being called in on line 6 that I named api, because I figured why not add some blanket protection against a ridiculous amount of requests- 1000 per user, per 25 minutes.

Now that's what I call protecting our API.

Final notes

That's pretty much all I got for now. I think the next best step, without a doubt, would be to start putting each major route in its own file (likely inside of a parent folder called routes/) and using express.Router to continue keeping your API modular and maintainable as it grows.

Oh, and before I forget- you probably noticed that in the ratelimits.js file, I'm adding a new property called statusCode, and also sending a response to the user each time they exceed the allowed limit- an object containing another status and an error string.

I've found that in order to send my own custom messages back to a frontend- aka, when wanting to let the user know the specific reason they're being rate-limited- I need to set the module's pre-made statusCode property to 200 to allow my custom response body to go all the way through. Technically a rate-limit rejection carries a status code of 429 back to the browser (which is the module's default) but yeah, for adding my own personal touch I had to force a statusCode: 200 and then just send the actual 429 myself in the response body.

Phew, ok I promise I'm done now... except to clarify that these examples were bare of any of other modules/imports for simplicity's sake, and that you probably are going to want to look into using things like cors and body-parser, among other things.

Alright, thanks everyone. Hopefully you found parts of this useful, and either way...

welcome to the limit!

- ZW

Back to posts