Building a Fraud Prevention Middleware in Node.js
Matt King
September 15, 2025
Fraud prevention middleware is a layer in your Node.js application that intercepts requests and evaluates them for fraud signals before they reach your business logic. Instead of scattering validation checks across controllers, middleware centralises the logic and applies it consistently to every relevant route.
This tutorial builds a production-ready Express middleware that validates emails, checks IP addresses, and blocks high-risk requests automatically.
What You'll Build
By the end of this guide, you'll have middleware that:
- Extracts the user's email and IP from incoming requests
- Validates them against Fidro's fraud detection API
- Blocks requests with disposable emails or high risk scores
- Caches results to avoid redundant API calls
- Fails open gracefully when the API is unreachable
Prerequisites
- Node.js 18+ and Express
- A Fidro API key (free plan includes 200 validations/month)
- Redis (optional, for caching)
Step 1: Create the Middleware File
// middleware/fraudPrevention.js
const axios = require('axios');
const FIDRO_API_KEY = process.env.FIDRO_API_KEY;
const FIDRO_BASE_URL = 'https://api.fidro.io/v1';
const RISK_THRESHOLD = 0.7;
const TIMEOUT_MS = 3000;
// Simple in-memory cache (use Redis in production)
const cache = new Map();
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
async function validateWithFidro(email, ip) {
const cacheKey = `${email}:${ip}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.result;
}
const response = await axios.post(
`${FIDRO_BASE_URL}/validate/email`,
{ email, ip },
{
headers: { Authorization: `Bearer ${FIDRO_API_KEY}` },
timeout: TIMEOUT_MS,
}
);
const result = response.data;
cache.set(cacheKey, { result, timestamp: Date.now() });
return result;
}
function fraudPrevention(options = {}) {
const threshold = options.riskThreshold || RISK_THRESHOLD;
const blockDisposable = options.blockDisposable !== false;
return async (req, res, next) => {
const email = req.body?.email;
const ip = req.ip || req.connection?.remoteAddress;
if (!email) return next();
try {
const validation = await validateWithFidro(email, ip);
// Block disposable emails
if (blockDisposable && validation.disposable) {
return res.status(422).json({
error: 'Please use a permanent email address.',
code: 'DISPOSABLE_EMAIL',
});
}
// Block high-risk requests
if (validation.risk_score > threshold) {
return res.status(422).json({
error: 'Unable to process this request.',
code: 'HIGH_RISK',
});
}
// Attach validation data for downstream use
req.fraudCheck = validation;
next();
} catch (error) {
// Fail open: allow the request if API is unreachable
console.warn('Fraud check failed, allowing request:', error.message);
req.fraudCheck = null;
next();
}
};
}
module.exports = fraudPrevention;
Step 2: Register the Middleware
Apply it to specific routes rather than globally. You only need fraud checks on signup, checkout, and similar sensitive endpoints:
const express = require('express');
const fraudPrevention = require('./middleware/fraudPrevention');
const app = express();
app.use(express.json());
// Apply to signup route
app.post('/api/signup', fraudPrevention(), async (req, res) => {
// req.fraudCheck contains the validation result
const user = await createUser(req.body);
res.json({ user });
});
// Apply to checkout with stricter threshold
app.post('/api/checkout', fraudPrevention({ riskThreshold: 0.5 }), async (req, res) => {
const order = await processOrder(req.body);
res.json({ order });
});
// No fraud check needed for public endpoints
app.get('/api/products', async (req, res) => {
res.json(await getProducts());
});
Step 3: Add Redis Caching for Production
The in-memory cache works for single-server deployments, but production applications need shared caching:
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function getCachedValidation(key) {
const cached = await redis.get(`fraud:${key}`);
return cached ? JSON.parse(cached) : null;
}
async function setCachedValidation(key, result) {
await redis.setex(`fraud:${key}`, 86400, JSON.stringify(result));
}
Step 4: Add Monitoring
Log blocked requests so you can monitor false positives and adjust thresholds:
if (validation.disposable) {
console.log('Blocked disposable email', {
email,
ip,
domain: email.split('@')[1],
timestamp: new Date().toISOString(),
});
}
Review these logs weekly for the first month. If legitimate users are being blocked, adjust your threshold or add domain allowlists.
How This Translates to Other Frameworks
The core logic is framework-agnostic. Here's the same pattern in Fastify:
// Fastify hook
fastify.addHook('preHandler', async (request, reply) => {
if (!request.body?.email) return;
const validation = await validateWithFidro(request.body.email, request.ip);
if (validation.disposable) {
reply.code(422).send({ error: 'Please use a permanent email address.' });
}
request.fraudCheck = validation;
});
Next Steps
- Get your free API key — 200 validations/month included
- Read the API documentation for the full response schema
- Deploy the middleware to staging and monitor for two weeks before going live
- Adjust your risk threshold based on observed false positive rates