Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ Swagger is controlled via environment variables:
- **Production**: Swagger is **disabled** by default (set `ENABLE_SWAGGER=true` to override)
- **Explicit override**: Set `ENABLE_SWAGGER=true` or `ENABLE_SWAGGER=false` to force enable/disable regardless of environment

## Rate limiting configuration

The application ships with a built‑in rate limiter based on [`express-rate-limit`](https://www.npmjs.com/package/express-rate-limit). It protects the entire API with a permissive window, and adds a more restrictive policy to
all `/api/auth` endpoints.

| Variable | Description | Default |
|----------------------|-------------------------------------------------------------|-----------------|
| `RATE_LIMIT_WINDOW_MS` | Time window in milliseconds | `900000` (15m) |
| `RATE_LIMIT_MAX` | Max requests per window for general routes | `100` |
| `AUTH_RATE_LIMIT_MAX` | Max requests per window for auth routes | `10` |

When a client exceeds the limit the server replies with `429 Too Many Requests` and a `Retry-After` header indicating how many seconds remain
in the current window.

## Example `.env` Configuration

```env
Expand All @@ -103,6 +117,11 @@ ENABLE_SWAGGER=true
# Or disable in development
NODE_ENV=development
ENABLE_SWAGGER=false

# Rate limiting (optional overrides)
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
AUTH_RATE_LIMIT_MAX=10
```

# 📌 How to Contribute
Expand Down
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"express-rate-limit": "^8.2.1",
"helmet": "^8.1.0",
"joi": "^18.0.2",
"jsonwebtoken": "^9.0.3",
Expand Down
52 changes: 52 additions & 0 deletions src/__tests__/rateLimiter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const request = require('supertest');

// helper to load app with fresh environment variables
function loadAppWithEnv(env = {}) {
// clear cache so that modules pick up new env values
jest.resetModules();
Object.assign(process.env, env);
return require('../app');
}

describe('Rate limiting middleware', () => {
beforeEach(() => {
// make sure we start from a clean state
jest.resetModules();
});

it('enforces the global limit and returns Retry-After header', async () => {
const app = loadAppWithEnv({
RATE_LIMIT_WINDOW_MS: '1000',
RATE_LIMIT_MAX: '2',
});

// first two requests should succeed
await request(app).get('/api/health').expect(200);
await request(app).get('/api/health').expect(200);

// third request should be blocked
const res = await request(app).get('/api/health').expect(429);
expect(res.headers['retry-after']).toBeDefined();
expect(res.body).toMatchObject({
success: false,
statusCode: 429,
message: expect.stringContaining('Too many requests'),
});
});

it('enforces a stricter limit on auth routes', async () => {
const app = loadAppWithEnv({
RATE_LIMIT_WINDOW_MS: '1000',
AUTH_RATE_LIMIT_MAX: '1',
});

// first auth request should go through (payload may be invalid, but not a rate-limit error)
await request(app).post('/api/auth/login').send({}).expect(res => {
expect([200, 400, 401, 422]).toContain(res.status);
});

// second request should hit the limit
const res = await request(app).post('/api/auth/login').send({}).expect(429);
expect(res.headers['retry-after']).toBeDefined();
});
});
8 changes: 6 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const helmet = require('helmet');
const morgan = require('morgan');
const { sendSuccess } = require('./utils/response');
const { getLoggerStream } = require('./utils/logger');
const { globalLimiter, authLimiter } = require('./middlewares/rateLimiter');
const authRoutes = require('./routes/auth.routes');
const protectedRoutes = require('./routes/protected.routes');

Expand All @@ -22,6 +23,9 @@ app.use(
);
app.use(helmet());

// rate limiting middleware
app.use(globalLimiter);

// Determine logging format based on environment
const morganFormat = process.env.NODE_ENV === 'production' ? 'combined' : 'dev';
app.use(morgan(morganFormat, { stream: getLoggerStream() }));
Expand All @@ -30,8 +34,8 @@ app.get('/api/health', (req, res) => {
sendSuccess(res, { status: 'ok' }, 200, 'Server is healthy');
});

// Auth routes
app.use('/api/auth', authRoutes);
// Auth routes (stricter rate limit)
app.use('/api/auth', authLimiter, authRoutes);

// Protected routes
app.use('/api/protected', protectedRoutes);
Expand Down
37 changes: 37 additions & 0 deletions src/middlewares/rateLimiter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const rateLimit = require('express-rate-limit');

// shared window for both global and auth limiters (milliseconds)
const WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 15 * 60 * 1000;

function createLimiter({ max }) {
return rateLimit({
windowMs: WINDOW_MS,
max,
standardHeaders: true, // Return rate limit info in the RateLimit-* headers
legacyHeaders: false, // Disable the X-RateLimit-* headers
handler: (req, res) => {
// express-rate-limit will automatically set Retry-After header if
// standardHeaders is enabled, but we send it explicitly for clarity.
const retryAfter = Math.ceil(WINDOW_MS / 1000);
res.set('Retry-After', String(retryAfter));

// Using the shared response format so tests and clients are consistent
res.status(429).json({
success: false,
statusCode: 429,
message: 'Too many requests, please try again later.',
data: {},
});
},
});
}

const globalLimiter = createLimiter({
max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100,
});

const authLimiter = createLimiter({
max: parseInt(process.env.AUTH_RATE_LIMIT_MAX, 10) || 10,
});

module.exports = { globalLimiter, authLimiter };