Node.js
Integrate redirect lookups with Express and Fastify using middleware and the Edge Query API
Overview
This guide shows you how to integrate redirect lookups into your Node.js application using Express or Fastify. You'll learn both real-time API fetching and startup-time syncing with in-memory caching.
Estimated time: ~15 minutes
Prerequisites:
- Node.js 20+ or 22 LTS
- Express 4.x/5.x or Fastify 4.x+ project
- API key from the dashboard
- Project ID from your redirect project
Target versions:
- Node.js 20/22 LTS (native fetch support)
- Express 4.x/5.x
- Fastify 4.x+
Request Flow
Here's how redirect lookups work in Node.js middleware:
flowchart LR
A[Client Request] --> B[Express/Fastify]
B --> C[Early Middleware/Hook]
C --> D{Redirect Check}
D -->|Match Found| E[res.redirect / reply.redirect]
D -->|No Match| F[next / return]
D -->|Error| F
E --> G[Redirect Response]
F --> H[Continue to Route Handler]Express: Pattern 1 — Real-Time Middleware
The real-time pattern calls the Edge Query API on every request to check for redirects. This keeps redirects always up-to-date.
Step 1: Create Redirect Middleware
Create a middleware function that fetches redirect rules:
async function redirectMiddleware(req, res, next) {
const path = req.path;
try {
const response = await fetch(
`https://api.3xx.app/v1/lookup?path=${encodeURIComponent(path)}&projectId=${process.env.REDIRECTIONS_PROJECT_ID}`,
{
headers: {
'X-API-Key': process.env.REDIRECTIONS_API_KEY,
},
}
);
if (response.status === 200) {
const data = await response.json();
return res.redirect(data.statusCode, data.destination);
}
// 204 = no match, pass through
next();
} catch (error) {
console.error('Redirect lookup failed:', error);
// On error, pass through to avoid breaking the site
next();
}
}
module.exports = redirectMiddleware;Native fetch: Node.js 18+ includes native fetch() support. No need for axios or node-fetch dependencies.
Step 2: Register Middleware Early
In your main app file, register the middleware before route handlers:
const express = require('express');
const redirectMiddleware = require('./middleware/redirects');
const app = express();
// Register redirect middleware EARLY (before routes)
app.use(redirectMiddleware);
// Your route handlers
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});Middleware order matters. The redirect middleware must come before your route handlers. If a route handler runs first, the middleware won't execute.
Step 3: Configure Environment Variables
Create a .env file in your project root:
REDIRECTIONS_API_KEY=YOUR_API_KEY
REDIRECTIONS_PROJECT_ID=YOUR_PROJECT_IDLoad environment variables using dotenv (Express 4.x) or native .env support (Node.js 20+):
// Option 1: dotenv (if using Express 4.x)
require('dotenv').config();
// Option 2: Node.js 20+ native support
// Run with: node --env-file=.env app.jsStep 4: Test the Integration
Start your server:
node app.jsTest with curl:
# Test a redirect
curl -I http://localhost:3000/test-path
# Should return:
# HTTP/1.1 301 Moved Permanently
# Location: https://example.com/destinationExpress: Pattern 2 — Startup-Time Sync + In-Memory Map
For high-traffic applications, fetch redirects once at startup and cache them in memory. This eliminates API calls on every request.
Step 1: Create Load Function
Create a function to fetch and parse redirects:
const redirectsMap = new Map();
async function loadRedirects() {
const apiKey = process.env.REDIRECTIONS_API_KEY;
const projectId = process.env.REDIRECTIONS_PROJECT_ID;
if (!apiKey || !projectId) {
throw new Error('Missing REDIRECTIONS_API_KEY or REDIRECTIONS_PROJECT_ID');
}
console.log('Loading redirects from API...');
const response = await fetch(
`https://api.3xx.app/v1/export?format=csv&projectId=${projectId}`,
{
headers: {
'X-API-Key': apiKey,
},
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
const csv = await response.text();
const lines = csv.split('\n').slice(1); // Skip header
redirectsMap.clear();
for (const line of lines) {
if (!line.trim()) continue;
const [path, destination, statusCode] = line.split(',');
redirectsMap.set(path, {
destination,
statusCode: parseInt(statusCode),
});
}
console.log(`✓ Loaded ${redirectsMap.size} redirects`);
}
function syncRedirectMiddleware(req, res, next) {
const redirect = redirectsMap.get(req.path);
if (redirect) {
return res.redirect(redirect.statusCode, redirect.destination);
}
next();
}
module.exports = { loadRedirects, syncRedirectMiddleware };Step 2: Load at Startup
Load redirects before starting the server:
const express = require('express');
const { loadRedirects, syncRedirectMiddleware } = require('./lib/redirects');
const app = express();
app.use(syncRedirectMiddleware);
app.get('/', (req, res) => {
res.send('Hello World');
});
async function start() {
// Load redirects before starting server
await loadRedirects();
// Refresh every 5 minutes
setInterval(() => {
loadRedirects().catch(err => {
console.error('Failed to refresh redirects:', err);
});
}, 5 * 60 * 1000);
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
}
start().catch(err => {
console.error('Startup failed:', err);
process.exit(1);
});Step 3: Test the Sync
Start your server and watch the console:
node app.js
# Output:
# Loading redirects from API...
# ✓ Loaded 42 redirects
# Server listening on port 3000Test redirects:
curl -I http://localhost:3000/test-pathAdd a new redirect in the dashboard, wait 5 minutes, then test again to verify the refresh works.
Fastify: Pattern 1 — Real-Time Hook
Fastify uses hooks instead of middleware. The onRequest hook is perfect for redirect checking.
Step 1: Create Redirect Hook
async function redirectPlugin(fastify, options) {
fastify.addHook('onRequest', async (request, reply) => {
const path = request.url;
try {
const response = await fetch(
`https://api.3xx.app/v1/lookup?path=${encodeURIComponent(path)}&projectId=${process.env.REDIRECTIONS_PROJECT_ID}`,
{
headers: {
'X-API-Key': process.env.REDIRECTIONS_API_KEY,
},
}
);
if (response.status === 200) {
const data = await response.json();
reply.redirect(data.statusCode, data.destination);
return; // Hook complete
}
// 204 = no match, continue
} catch (error) {
console.error('Redirect lookup failed:', error);
// On error, continue to avoid breaking the site
}
});
}
module.exports = redirectPlugin;Fastify hooks don't use next(): In Fastify, hooks either send a response (like reply.redirect()) or return to continue the request lifecycle.
Step 2: Register Plugin
Register the plugin in your main app:
const fastify = require('fastify')({ logger: true });
const redirectPlugin = require('./plugins/redirects');
// Register redirect plugin
fastify.register(redirectPlugin);
// Your routes
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
async function start() {
try {
await fastify.listen({ port: 3000 });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
}
start();Step 3: Test with Curl
curl -I http://localhost:3000/test-pathFastify: Pattern 2 — Startup-Time Sync + In-Memory Map
For high-traffic Fastify applications, sync redirects at startup:
Step 1: Create Sync Plugin
const redirectsMap = new Map();
async function loadRedirects() {
const apiKey = process.env.REDIRECTIONS_API_KEY;
const projectId = process.env.REDIRECTIONS_PROJECT_ID;
if (!apiKey || !projectId) {
throw new Error('Missing REDIRECTIONS_API_KEY or REDIRECTIONS_PROJECT_ID');
}
console.log('Loading redirects from API...');
const response = await fetch(
`https://api.3xx.app/v1/export?format=csv&projectId=${projectId}`,
{
headers: {
'X-API-Key': apiKey,
},
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
const csv = await response.text();
const lines = csv.split('\n').slice(1); // Skip header
redirectsMap.clear();
for (const line of lines) {
if (!line.trim()) continue;
const [path, destination, statusCode] = line.split(',');
redirectsMap.set(path, {
destination,
statusCode: parseInt(statusCode),
});
}
console.log(`✓ Loaded ${redirectsMap.size} redirects`);
}
async function redirectsSyncPlugin(fastify, options) {
// Load at startup
await loadRedirects();
// Refresh every 5 minutes
setInterval(() => {
loadRedirects().catch(err => {
fastify.log.error('Failed to refresh redirects:', err);
});
}, 5 * 60 * 1000);
// Hook to check redirects
fastify.addHook('onRequest', async (request, reply) => {
const redirect = redirectsMap.get(request.url);
if (redirect) {
reply.redirect(redirect.statusCode, redirect.destination);
}
});
}
module.exports = redirectsSyncPlugin;Step 2: Register Plugin
const fastify = require('fastify')({ logger: true });
const redirectsSyncPlugin = require('./plugins/redirects-sync');
fastify.register(redirectsSyncPlugin);
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
async function start() {
try {
await fastify.listen({ port: 3000 });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
}
start();Step 3: Test the Sync
Start the server and verify redirects load:
node app.js
# Output:
# Loading redirects from API...
# ✓ Loaded 42 redirects
# Server listening at http://127.0.0.1:3000Shared Patterns
TypeScript Types
For TypeScript projects, define types for API responses:
export interface RedirectResponse {
path: string;
destination: string;
statusCode: number;
projectId: string;
}
export interface RedirectRule {
destination: string;
statusCode: number;
}Use these types in your middleware/hooks:
import type { RedirectResponse, RedirectRule } from './types/redirects';
const response = await fetch(/* ... */);
const data: RedirectResponse = await response.json();Error Handling
Always wrap API calls in try/catch and continue on error:
try {
const response = await fetch(/* ... */);
// Handle response
} catch (error) {
console.error('Redirect lookup failed:', error);
// Don't break the request - pass through
next(); // Express
return; // Fastify
}Environment Variable Configuration
Both Express and Fastify can use the same .env file:
REDIRECTIONS_API_KEY=YOUR_API_KEY
REDIRECTIONS_PROJECT_ID=YOUR_PROJECT_IDLoad with dotenv or Node.js 20+ native support:
// Option 1: dotenv
require('dotenv').config();
// Option 2: Node.js 20+ --env-file flag
// Run: node --env-file=.env app.jsFallback Behavior
Implement graceful fallback to avoid breaking your application:
Express:
try {
// API call
} catch (error) {
console.error('Redirect lookup failed:', error);
next(); // Continue to next middleware/route
}Fastify:
try {
// API call
} catch (error) {
fastify.log.error('Redirect lookup failed:', error);
// Return without sending response = continue
}Advanced: Local Caching
For even better performance, implement local caching with TTL:
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function cachedLookup(path) {
const cached = cache.get(path);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const response = await fetch(/* API call */);
const data = await response.json();
cache.set(path, {
data,
timestamp: Date.now(),
});
return data;
}Testing
Express Testing
# Start server
node app.js
# Test redirect
curl -I http://localhost:3000/test-path
# Test non-existent path (should pass through)
curl -I http://localhost:3000/no-redirect
# Test server restart with sync pattern
# (add redirect in dashboard, restart server, verify new redirect works)Fastify Testing
# Start server
node app.js
# Test redirect
curl -I http://localhost:3000/test-path
# Test with verbose output
curl -v http://localhost:3000/test-path
# Verify sync refresh (after 5 minutes)
# Add new redirect in dashboard, wait 5 min, testTroubleshooting
Middleware Order (Express)
Symptom: Redirects don't work, routes handle requests first.
Cause: Redirect middleware registered after route handlers.
Fix: Move app.use(redirectMiddleware) before route definitions.
Async Middleware in Express 4.x
Symptom: Errors in async middleware aren't caught.
Cause: Express 4.x doesn't natively support async middleware error handling.
Fix: Wrap async middleware or use Express 5.x:
// Express 4.x: wrap in error handler
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.use(asyncHandler(redirectMiddleware));Express 5.x handles async middleware natively.
Fastify Hook Order
Symptom: Redirects execute after other logic.
Cause: Plugin registered too late.
Fix: Register redirect plugin early, before route-specific plugins.
Fetch Not Available
Symptom: fetch is not defined error.
Cause: Node.js version < 18.
Fix: Upgrade to Node.js 18+ or use a fetch polyfill:
// Install: npm install node-fetch
const fetch = require('node-fetch');For more help, see the Troubleshooting Guide.