Integrations → Node.js

Node.js integration

Express middleware, Fastify hook, or plain fetch(). Async, zero dependencies, works on every hosting from Vercel to self-hosted PM2.

Express middleware

Drop in zerobot.js and app.use(zerobot) before your routes:

// zerobot.js
const LICENSE = process.env.ZEROBOT_KEY;
const DOMAIN  = process.env.ZEROBOT_DOMAIN;

async function zerobot(req, res, next) {
  try {
    const ip = req.headers['cf-connecting-ip'] || req.ip;
    const url = 'https://api.zerobot.info/v3/openapi?' + new URLSearchParams({
      license:   LICENSE,
      ip,
      domain:    DOMAIN,
      useragent: req.headers['user-agent'] || '',
    });

    const controller = new AbortController();
    const timeout    = setTimeout(() => controller.abort(), 5000);

    const r    = await fetch(url, { signal: controller.signal });
    clearTimeout(timeout);
    const data = await r.json();

    if (data.is_bot) {
      return res.status(403).send(`Blocked: ${data.reason}`);
    }
  } catch (err) {
    // Fail open on timeout / network error
    console.warn('ZeroBot unreachable, failing open', err.message);
  }
  next();
}

module.exports = zerobot;
// app.js
const express = require('express');
const zerobot = require('./zerobot');

const app = express();
app.use(zerobot);           // screens every request
app.get('/', (req, res) => res.send('Hello, human!'));
app.listen(3000);

Fastify hook

const fastify = require('fastify')();

fastify.addHook('onRequest', async (req, reply) => {
  const url = 'https://api.zerobot.info/v3/openapi?' + new URLSearchParams({
    license:   process.env.ZEROBOT_KEY,
    ip:        req.ip,
    domain:    process.env.ZEROBOT_DOMAIN,
    useragent: req.headers['user-agent'] ?? '',
  });
  const res  = await fetch(url, { signal: AbortSignal.timeout(5000) });
  const data = await res.json();
  if (data.is_bot) {
    reply.code(403).send({ error: data.reason });
  }
});

Next.js middleware (App Router)

// middleware.ts — runs on every request at the edge
import { NextRequest, NextResponse } from 'next/server';

export async function middleware(req: NextRequest) {
  const ip = req.headers.get('cf-connecting-ip') || req.ip || '';
  const url = 'https://api.zerobot.info/v3/openapi?' + new URLSearchParams({
    license:   process.env.ZEROBOT_KEY!,
    ip,
    domain:    process.env.ZEROBOT_DOMAIN!,
    useragent: req.headers.get('user-agent') || '',
  });
  const data = await fetch(url).then(r => r.json()).catch(() => ({}));
  if (data.is_bot) {
    return new NextResponse(`Blocked: ${data.reason}`, { status: 403 });
  }
}

Caching (recommended)

To avoid hitting the API on every pageview, cache the verdict per IP for 1–24 hours. Simple in-memory Map for single-instance apps, Redis for clusters:

const cache = new Map();
const TTL   = 24 * 3600 * 1000; // 24h

async function screen(req) {
  const ip = req.ip;
  const hit = cache.get(ip);
  if (hit && hit.expires > Date.now()) return hit.verdict;

  const data = await fetch(/* as above */).then(r => r.json());
  cache.set(ip, { verdict: data, expires: Date.now() + TTL });
  return data;
}