Middleware System Documentation¶
Breadcrumbs: Documentation > Guides > API > Middleware
This guide explains the Next.js middleware implementation for request tracking and webhook integration.
Table of Contents¶
- Overview
- Architecture
- Webhook System
- Configuration
- Usage Examples
- Security Considerations
- Troubleshooting
Overview¶
The middleware system intercepts all requests to the portfolio website and:
- Tracks request metadata (URL, method, headers, timestamp)
- Sends request data to configured webhooks (optional)
- Adds debug headers in development mode
- Handles errors gracefully without blocking requests
Key features:
- Non-blocking - Webhook failures don't affect page loads
- Configurable - Enable/disable via environment variables
- Secure - HTTPS-only webhooks in production
- Selective - Configure which headers to include
- Performant - Minimal overhead on request processing
Architecture¶
Files¶
Request Flow¶
1. User Request → Next.js
2. Middleware intercepts
3. Process request metadata
4. Send to webhook (async, non-blocking)
5. Continue to page/API route
// Simplified flow
export async function middleware(request: NextRequest) {
const response = NextResponse.next(); // Continue request
// Track in background (doesn't block response)
sendToWebhook(request).catch(console.error);
return response; // Response sent immediately
}
Webhook System¶
What is the Webhook?¶
The webhook sends request information to an external endpoint (e.g., Discord, Slack, custom analytics).
Use cases:
- Monitor site traffic
- Track user behavior
- Debug production issues
- Analytics dashboard
- Security monitoring
Webhook Format¶
Request payload:
Breakdown:
- Timestamp: Request time in Europe/Brussels timezone
- Method: HTTP method (GET, POST, etc.)
- URL: Full request URL with query params
Discord Integration Example¶
Setup Discord webhook:
- Go to Discord Server Settings → Integrations → Webhooks
- Click "New Webhook"
- Copy webhook URL
- Add to
.env:
Result:
Requests appear in Discord channel:
Custom Webhook Server¶
Simple Express.js webhook receiver:
// webhook-server.js
const express = require("express");
const app = express();
app.use(express.json());
app.post("/webhook", (req, res) => {
const { content } = req.body;
console.log("📊 Request received:", content);
// Store in database, send alert, etc.
res.status(200).send("OK");
});
app.listen(3001, () => {
console.log("Webhook server listening on port 3001");
});
Run:
Configure:
Configuration¶
Environment Variables¶
Required:
Optional (defaults shown):
Configuration File¶
File: middleware/config.ts
export const webhookConfig = {
url: process.env.WEBHOOK_URL || null,
enabled: process.env.WEBHOOK_ENABLED === "true",
includeHeaders: ["user-agent", "referer", "accept", "accept-language", "x-forwarded-for"],
};
Configuration options:
| Option | Type | Description |
|---|---|---|
url |
string |
Webhook URL (HTTPS required in prod) |
enabled |
boolean |
Enable/disable webhook |
includeHeaders |
string[] |
List of headers to include in webhook |
Path Exclusions¶
Excluded paths (not tracked):
export const excludedPaths = [
"api", // API routes
"_next/static", // Static files
"_next/image", // Image optimization
"favicon.ico", // Favicon
"robots.txt", // Robots file
"sitemap.xml", // Sitemap
];
Matcher configuration:
// middleware.ts
export const config = {
matcher: ["/((?!api|_next/static|_next/image|_next/data|favicon.ico|robots.txt|sitemap.xml).*)"],
};
This regex excludes the paths above from middleware processing.
Custom Header Inclusion¶
Add custom headers to track:
// middleware/config.ts
export const webhookConfig = {
// ... other config
includeHeaders: [
"user-agent",
"referer",
"accept-language",
"x-forwarded-for",
"x-real-ip", // Add custom header
"cf-connecting-ip", // Cloudflare IP
],
};
Access in webhook:
// middleware.ts
const headers: Record<string, string> = {};
webhookConfig.includeHeaders.forEach((header) => {
const value = request.headers.get(header);
if (value) headers[header] = value;
});
// Send headers in webhook payload
await fetch(webhookConfig.url, {
method: "POST",
body: JSON.stringify({ content, headers }),
});
Usage Examples¶
Example 1: Track Page Views¶
Goal: Count visits to each project page.
// middleware.ts
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
// Extract page path
const path = request.nextUrl.pathname;
// Check if project page
if (path.startsWith("/projects/") && path !== "/projects") {
const slug = path.replace("/projects/", "");
// Send to analytics webhook
await fetch(webhookConfig.url, {
method: "POST",
body: JSON.stringify({
event: "project_view",
slug,
timestamp: new Date().toISOString(),
userAgent: request.headers.get("user-agent"),
}),
}).catch(console.error);
}
return response;
}
Example 2: Geo-Location Tracking¶
Track visitor locations:
// middleware.ts
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
// Get IP address
const ip =
request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "unknown";
// Get country (if using Vercel/Cloudflare)
const country = request.geo?.country || "Unknown";
const city = request.geo?.city || "Unknown";
// Send geo data
await fetch(webhookConfig.url, {
method: "POST",
body: JSON.stringify({
event: "page_view",
path: request.nextUrl.pathname,
country,
city,
ip,
}),
}).catch(console.error);
return response;
}
Example 3: Bot Detection¶
Track bot visits separately:
// middleware.ts
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const userAgent = request.headers.get("user-agent") || "";
// Detect bots
const isBot = /bot|crawler|spider|scraper/i.test(userAgent);
if (isBot) {
await fetch(webhookConfig.url, {
method: "POST",
body: JSON.stringify({
event: "bot_visit",
userAgent,
path: request.nextUrl.pathname,
timestamp: new Date().toISOString(),
}),
}).catch(console.error);
}
return response;
}
Example 4: Rate Limiting¶
Basic rate limiting implementation:
// middleware.ts
const rateLimitMap = new Map<string, number[]>();
export async function middleware(request: NextRequest) {
const ip = request.headers.get("x-forwarded-for")?.split(",")[0] || "unknown";
// Get request timestamps for this IP
const timestamps = rateLimitMap.get(ip) || [];
const now = Date.now();
// Remove timestamps older than 1 minute
const recentTimestamps = timestamps.filter((t) => now - t < 60000);
// Check if over limit (e.g., 100 requests per minute)
if (recentTimestamps.length >= 100) {
return new NextResponse("Rate limit exceeded", { status: 429 });
}
// Add current timestamp
recentTimestamps.push(now);
rateLimitMap.set(ip, recentTimestamps);
return NextResponse.next();
}
Security Considerations¶
HTTPS Enforcement¶
Production requirement:
// middleware/config.ts
if (appConfig.isProduction && webhookUrl.protocol !== "https:") {
console.warn("Webhook URL must be HTTPS in production");
webhookConfig.url = null; // Disable
}
Why: Prevent leaking sensitive data over unencrypted connections.
Header Sanitization¶
Only include safe headers:
// ❌ BAD: Including sensitive headers
includeHeaders: [
"authorization", // Contains auth tokens!
"cookie", // Contains session data!
"x-api-key", // Contains API keys!
];
// ✅ GOOD: Safe headers only
includeHeaders: ["user-agent", "referer", "accept-language"];
Error Handling¶
Never throw errors in middleware:
// ✅ GOOD: Catch all errors
try {
await fetch(webhookConfig.url, {
method: "POST",
body: JSON.stringify({ content }),
}).catch((error) => {
console.error("Webhook error:", error);
});
} catch (e) {
console.error("Middleware error:", e);
}
// Request continues regardless
return response;
Environment Validation¶
Validate webhook URL:
// middleware/config.ts
if (webhookConfig.url) {
try {
const url = new URL(webhookConfig.url);
if (url.protocol !== "https:" && appConfig.isProduction) {
webhookConfig.url = null;
}
} catch (error) {
console.error("Invalid webhook URL:", error);
webhookConfig.url = null;
}
}
Troubleshooting¶
Issue: Webhook not firing¶
Checklist:
- Check environment variables:
- Verify webhook is enabled:
- Check path is not excluded:
# These paths are excluded by default:
/api/*
/_next/static/*
/_next/image/*
/favicon.ico
/robots.txt
/sitemap.xml
- Test webhook URL manually:
curl -X POST "https://your-webhook.com" \
-H "Content-Type: application/json" \
-d '{"content":"Test message"}'
Issue: Webhook errors in logs¶
Common errors:
1. Network timeout:
Solution: Check webhook URL is accessible from your server.
2. Invalid JSON:
Solution: Webhook endpoint might be returning HTML instead of JSON.
3. SSL certificate error:
Solution: Use valid SSL certificate for webhook endpoint.
Issue: Middleware not running¶
Check Next.js version:
Verify middleware.ts location:
Check config matcher:
// middleware.ts - Must export config
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*))"],
};
Issue: Performance impact¶
If middleware slows down requests:
- Disable webhook in development:
- Use async fire-and-forget:
// Don't await webhook
fetch(webhookConfig.url, { ... }).catch(console.error);
return response; // Return immediately
- Increase timeout:
await fetch(webhookConfig.url, {
method: "POST",
body: JSON.stringify({ content }),
signal: AbortSignal.timeout(5000), // 5 second timeout
}).catch(console.error);
See Also¶
- LLMs.txt Documentation - AI agent discovery endpoint
- File Download API - Secure file downloads
- Best Practices - Code patterns
- Configuration - Environment variables
Next Steps¶
- Set up webhook endpoint (Discord, Slack, custom)
- Configure environment variables
- Test in development
- Monitor webhook logs
- Implement custom tracking logic
Last Updated: February 2026
Maintainers: Simon Stijnen
Questions? Open an issue on GitHub