Server
Node.js
Protect an Express, Fastify, Koa or Next.js site from bots with one middleware — no dependencies.
Overview
This guide adds BlockBots to a Node.js server. You will install one package and mount one middleware — detection then runs on every request before your routes do.
It takes about five minutes. No dependencies, no database, no build step.
This guide covers server-side protection. If your pages are rendered entirely in the browser, see the JavaScript guide. The two work together.
Prerequisites
- Node 18 or higher
- An API key — create one at panel.blockbots.org under Settings → API Keys
- The domain registered on that key, exactly as visitors reach it
Step 1 — Install
npm install @blockbots/node
Step 2 — Mount the middleware
Add it before your routes:
const express = require('express');
const blockbots = require('@blockbots/node');
const app = express();
app.use(blockbots({
apiKey: 'YOUR_API_KEY',
}));
app.get('/', (req, res) => res.send('Welcome'));
app.listen(3000);
There is nothing to call. The middleware decides on its own and answers the requests it stops.
Order matters. A middleware mounted after your routes never sees a request the router has already answered, so nothing is protected. It writes a line to your log when that happens, so check there if nothing seems to happen.
Read the key from the environment instead of hard-coding it:
app.use(blockbots({ apiKey: process.env.BLOCKBOTS_API_KEY }));
BLOCKBOTS_API_KEY is picked up automatically, so blockbots() with no arguments also works when that variable is set.
Step 3 — Exclude what must not be checked
Health checks, webhooks and your own API routes should go straight through:
app.use(blockbots({
apiKey: process.env.BLOCKBOTS_API_KEY,
exclude: ['/healthz', '/webhooks', '/api'],
}));
A string matches that path and everything under it — /api covers /api/users, but not /api-docs. Regular expressions and functions work too:
exclude: [
'/webhooks', // path and everything under it
/^\/admin\//, // regular expression
(req) => req.headers['x-internal'] === '1' // your own rule
]
Step 4 — Check it works
Open your site in a private window. You should see a verification screen for a second, then your own page.
To confirm what is going on under the hood, switch on the self-test:
app.use(blockbots({
apiKey: process.env.BLOCKBOTS_API_KEY,
selftest: 'pick-a-long-secret',
}));
Then request any page with that secret:
curl "https://your-site.com/?bb-selftest=pick-a-long-secret"
A healthy install answers like this:
Blockbots self-test
environment
node v20.11.0
platform generic (has a socket)
ed25519 yes
library active yes
enabled option true
service state ok
this request
socket address 203.0.113.9
trusted proxy NO
resolved client ip 203.0.113.9
ip came from socket
looks like a browser yes
navigation yes
host your-site.com
path excluded NO
token
__bbjwt cookie absent - visitor has not been verified yet
cache
store memory
entries 14
ttl 3600s
api
url https://api.blockbots.org/v1/verify-bot
round trip 86 ms
reachable yes
answered not_bot
Nothing secret is printed — not your API key, not the visitor's token. Remove the selftest option when you are done; without it the page does not exist.
Step 5 — If you use a CDN or reverse proxy
Cloudflare is recognised automatically, and so are nginx, Docker and a load balancer on your own network. Any other CDN needs one line, otherwise every visitor arrives as the same address and nobody can pass verification:
app.use(blockbots({
apiKey: process.env.BLOCKBOTS_API_KEY,
trustedProxies: '203.0.113.0/24,198.51.100.7',
}));
Use the address ranges your CDN connects from — your provider publishes them.
In the self-test, trusted proxy should read yes and resolved client ip should be the visitor's real address, not your CDN's.
Express's own
app.set('trust proxy', …)does not affect BlockBots. UsetrustedProxies.
Step 6 — Other frameworks
Fastify
Fastify runs Connect-style middleware through @fastify/middie:
const fastify = require('fastify')();
const blockbots = require('@blockbots/node');
await fastify.register(require('@fastify/middie'));
fastify.use(blockbots({ apiKey: process.env.BLOCKBOTS_API_KEY }));
fastify.get('/', async () => 'Welcome');
await fastify.listen({ port: 3000 });
Next.js
Next.js middleware runs on a Web Request, so use check() with toResponse():
import { createBlockbots, toResponse } from '@blockbots/node';
const bb = createBlockbots({ apiKey: process.env.BLOCKBOTS_API_KEY });
export async function middleware(request) {
const decision = await bb.check(request);
if (!decision.allow) return toResponse(decision);
}
export const config = { matcher: ['/((?!_next|favicon.ico).*)'] };
Vercel, Cloudflare Workers, Netlify and Deno Deploy work as-is — each is recognised from its own environment. Anywhere else there is no connection to read the visitor's address from, so name the header your platform sets:
const bb = createBlockbots({
apiKey: process.env.BLOCKBOTS_API_KEY,
clientIp: 'x-real-ip',
});
Without it protection switches itself off for those requests and says so in your log — deliberately, because a cookie with no address to bind to would send every visitor back to the verification page forever.
Koa, Hapi and everything else
Use check(). It reads the request and gives you a verdict without touching the response, so you decide what happens:
const { createBlockbots } = require('@blockbots/node');
const bb = createBlockbots({ apiKey: process.env.BLOCKBOTS_API_KEY });
// Koa
app.use(async (ctx, next) => {
const decision = await bb.check(ctx.req);
if (decision.allow) return next();
ctx.status = decision.response.status;
ctx.set(decision.response.headers);
ctx.body = decision.response.body;
});
What you get back:
{
action: 'allow', // 'allow' | 'block' | 'challenge'
allow: true, // the one field most code needs
reason: 'token_valid', // why, for your logs
ip: '203.0.113.9', // the resolved client address
ua: 'Mozilla/5.0 …',
response: { … } // the page to send, on 'block' and 'challenge'
}
Send response as-is and the flow works exactly as with the middleware. Ignore it and nothing is enforced — useful if you only want to log at first.
The middleware also leaves this object on req.blockbots, so a route behind it can read req.blockbots.ip without doing the work again.
How it works
- A visitor with a valid cookie passes through instantly — no network call.
- Anything that does not look like a browser is checked against the cloud. Crawlers you allow, such as Googlebot, get your content; impostors using their name get
403. - Everyone else sees the verification page, gets a signed cookie, and the page reloads.
Verdicts are cached, so a crawler costs one cloud call per hour rather than one per page. fetch and XHR requests are never interrupted, and POST bodies are never swallowed. If the cloud is unreachable, the middleware steps aside and your site keeps serving.
A dead or expired key never reaches your visitors. Before the verification page is served for the first time, BlockBots asks whether the cloud is serving your key at all. A rejection switches protection off for five minutes rather than handing out a page whose script would fail the same way and never reload — so a lapsed subscription leaves your site unprotected, not broken. The answer is cached, so this costs one request an hour, not one per visitor, and renewing a subscription takes effect on its own with no restart.
Next steps
- JavaScript — add the browser SDK alongside this
- Python — the same protection for Django or FastAPI
- Go — the same protection for a net/http server
- PHP — the same protection for a PHP site