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

terminal
npm install @blockbots/node

Step 2 — Mount the middleware

Add it before your routes:

server.js
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:

server.js
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:

server.js
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:

server.js
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:

server.js
app.use(blockbots({
  apiKey: process.env.BLOCKBOTS_API_KEY,
  selftest: 'pick-a-long-secret',
}));

Then request any page with that secret:

terminal
curl "https://your-site.com/?bb-selftest=pick-a-long-secret"

A healthy install answers like this:

output
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:

server.js
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. Use trustedProxies.

Step 6 — Other frameworks

Fastify

Fastify runs Connect-style middleware through @fastify/middie:

server.js
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():

middleware.js
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:

middleware.js
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:

middleware.js
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:

js
{
  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

  1. A visitor with a valid cookie passes through instantly — no network call.
  2. 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.
  3. 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