BotBye! integration for NestJS with Fastify applications.
Install
1
npm i @botbye/nest-fastify
1
yarn add @botbye/nest-fastify
Requires @nestjs/common >= 10 and fastify >= 4 as peer dependencies.
Configuration
Register BotByeModule once in your root module:
1
2
3
4
5
6
7
8
9
10
11
12
import { Module } from "@nestjs/common";
import { BotByeModule } from "@botbye/nest-fastify";
@Module({
imports: [
BotByeModule.register({
// Use your project server-key
serverKey: "00000000-0000-0000-0000-000000000000",
}),
],
})
export class AppModule {}
init options
| Option | Type | Required | Description |
|---|---|---|---|
| serverKey | string | Yes | Server key from your BotBye project |
| url | string | No | Override BotBye API endpoint (default: https://verify.botbye.com) |
| logger.level | "error" "warn" "info" "debug" "log" | No | Log level (default: "info") |
| logger.logger | TLogger | No | Custom logger instance implementing { error, warn, info, debug, log } |
| timeouts.evaluate | number | No | Timeout in milliseconds for each evaluate call |
| tokenExtractor | (request: FastifyRequest) => Nullable<string> | No* | Extracts the BotBye token from the Fastify request. *Required when using BotByeMiddleware — without it the middleware skips evaluation and returns an error result. |
Usage
After registering BotByeModule, inject TBotByeService using BOTBYE_SERVICE_DI_TOKEN to call evaluate from guards, controllers, or services.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Controller, Post, Inject, Req, ForbiddenException } from "@nestjs/common";
import { FastifyRequest } from "fastify";
import { BOTBYE_SERVICE_DI_TOKEN, TBotByeService } from "@botbye/nest-fastify";
@Controller()
export class AppController {
constructor(
@Inject(BOTBYE_SERVICE_DI_TOKEN) private readonly botbye: TBotByeService
) {}
@Post("/api/submit")
async submit(@Req() req: FastifyRequest) {
const result = await this.botbye.evaluate({
type: "validate",
request: {
request: req,
// "x-botbye-token" is an example — pass the token from wherever you store it
token: req.headers["x-botbye-token"] as string | null,
},
});
if (result.decision === "BLOCK") {
throw new ForbiddenException();
}
// proceed normally
}
}
There are three event types — validate, risk, and full — each suited for a different layer of your application.
validate — edge-level bot check
Use at the edge — guard, route handler, middleware — when you just want to know: was this request made by a bot? No user or domain context needed.
Event fields:
1
2
3
4
5
6
7
8
9
10
11
12
{
type: "validate";
request:
// Option A: pass the Fastify request object directly — SDK extracts everything automatically
| { request: FastifyRequest; token?: string | null }
// Option B: construct request info manually
| { ip: string; headers: Record<string, string>; requestMethod?: string | null; requestUri?: string | null; token?: string | null };
customFields?: Record<string, string>;
}
The SDK extracts IP, headers, method, and URI from the Fastify request automatically. You can also pass request info manually — see Option B above. The token is a one-time token generated by the BotBye client-side SDK that contains information about the user's device. Pass whatever the client sent; if no token is received, the decision will be "BLOCK".
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Injectable, CanActivate, ExecutionContext, Inject } from "@nestjs/common";
import { FastifyRequest } from "fastify";
import { BOTBYE_SERVICE_DI_TOKEN, TBotByeService } from "@botbye/nest-fastify";
@Injectable()
export class BotByeGuard implements CanActivate {
constructor(
@Inject(BOTBYE_SERVICE_DI_TOKEN) private readonly botbye: TBotByeService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<FastifyRequest>();
const result = await this.botbye.evaluate({
type: "validate",
request: {
request,
// "x-botbye-token" is an example — pass the token from wherever you store it
token: request.headers["x-botbye-token"] as string | null,
},
});
return result.decision !== "BLOCK";
}
}
risk — domain-level risk scoring
Use inside services that already know the user: auth, payments, account management, etc. The purpose shifts from "is this a bot?" to "is something suspicious happening for this user?" — credential stuffing, account takeover, account sharing, logins from a new geo.
Event fields:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
type: "risk";
request:
// Only ip is needed at this level
| { ip: string; headers?: Record<string, string>; requestMethod?: string | null; requestUri?: string | null; token?: string | null }
// Fastify request object also accepted if convenient
| { request: FastifyRequest };
event: {
type: string; // e.g. "login", "password_change", "checkout"
status: "ATTEMPTED" | "SUCCESSFUL" | "FAILED" | "UNKNOWN";
};
user: {
accountId: string;
username?: string | null;
email?: string | null;
phone?: string | null;
};
customFields?: Record<string, string>;
botbyeResult?: string; // if a validate call was made earlier, pass its result.botbyeResult here to link the requests; omit if there was no prior validate
}
event and user are the key fields here — they define what action is being performed and who is performing it, which is what drives the risk score. ip is equally important: BotBye tracks which IPs access the account to detect patterns like account sharing, credential stuffing, and suspicious geo logins. Pass it directly as { ip }, or pass the Fastify request object if that's more convenient.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Injectable, Inject } from "@nestjs/common";
import { BOTBYE_SERVICE_DI_TOKEN, TBotByeService } from "@botbye/nest-fastify";
@Injectable()
export class AuthService {
constructor(
@Inject(BOTBYE_SERVICE_DI_TOKEN) private readonly botbye: TBotByeService
) {}
async onLoginAttempt(ip: string, userId: string, email: string, loginSucceeded: boolean) {
const result = await this.botbye.evaluate({
type: "risk",
request: { ip },
event: {
type: "login",
status: loginSucceeded ? "SUCCESSFUL" : "FAILED",
},
user: {
accountId: userId,
email,
},
});
if (result.decision === "BLOCK") {
// Lock account, trigger MFA, send alert, etc.
}
}
}
Linking validate and risk events
When the same request is evaluated at two layers — for example, once at the edge in a guard (type: "validate") and then again inside a domain service (type: "risk") — BotBye can link both events and display them as a single event in the dashboard.
Step 1 — guard (edge layer): run validate and capture botbye_result:
1
2
3
4
5
6
7
8
9
10
11
// e.g. in BotByeGuard.canActivate()
const edgeResult = await this.botbye.evaluate({
type: "validate",
request: {
request,
// "x-botbye-token" is an example — pass the token from wherever you store it
token: request.headers["x-botbye-token"] as string | null,
},
});
const edgeBotbyeResult = edgeResult.botbye_result;
// Pass edgeBotbyeResult downstream — request object, function argument, shared context, etc.
Step 2 — domain service (auth, payment, account management): pass it as botbyeResult in the risk call:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// e.g. in AuthService.onLoginAttempt()
const riskResult = await this.botbye.evaluate({
type: "risk",
request: { ip },
event: {
type: "login",
status: loginSucceeded ? "SUCCESSFUL" : "FAILED",
},
user: {
accountId: userId,
email,
},
botbyeResult: edgeBotbyeResult,
});
botbye_result is optional in the response — if it is absent, omit botbyeResult and the events will be recorded independently.
full — edge check and domain scoring in one call
Use when you have all context at once: raw request, token, user, and event. Equivalent to running validate and risk in a single call. A login endpoint is a typical example — it receives the HTTP request and immediately knows the user and outcome.
Event fields:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
type: "full";
request:
| { request: FastifyRequest; token?: string | null }
| { ip: string; headers: Record<string, string>; requestMethod?: string | null; requestUri?: string | null; token?: string | null };
event: {
type: string;
status: "ATTEMPTED" | "SUCCESSFUL" | "FAILED" | "UNKNOWN";
};
user: {
accountId: string;
username?: string | null;
email?: string | null;
phone?: string | null;
};
customFields?: Record<string, string>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { Injectable, Inject, ForbiddenException } from "@nestjs/common";
import { FastifyRequest } from "fastify";
import { BOTBYE_SERVICE_DI_TOKEN, TBotByeService } from "@botbye/nest-fastify";
@Injectable()
export class AuthService {
constructor(
@Inject(BOTBYE_SERVICE_DI_TOKEN) private readonly botbye: TBotByeService
) {}
async login(request: FastifyRequest, email: string, password: string) {
const user = await this.findUser(email);
const loginSucceeded = user && (await this.checkPassword(user, password));
const result = await this.botbye.evaluate({
type: "full",
request: {
request,
// "x-botbye-token" is an example — pass the token from wherever you store it
token: request.headers["x-botbye-token"] as string | null,
},
event: {
type: "login",
status: loginSucceeded ? "SUCCESSFUL" : "FAILED",
},
user: {
accountId: user?.id ?? "unknown",
email,
},
});
if (result.decision === "BLOCK") {
throw new ForbiddenException();
}
// proceed normally
}
}
Response
evaluate always returns a Promise<TEvaluationResult>:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type TEvaluationResult =
| {
decision: "ALLOW" | "BLOCK" | "CHALLENGE";
request_id: string;
risk_score: number;
scores: Record<string, number>;
signals: string[];
botbye_result?: string;
}
| {
decision: "ALLOW";
botbye_result?: string;
error: { message: string };
};
Check result.decision to decide how to handle the request:
- "ALLOW" — request appears legitimate, proceed normally
- "BLOCK" — bot or suspicious activity detected, block the request
- "CHALLENGE" — uncertain, consider issuing a CAPTCHA, MFA, or additional verification step
When the response contains an error field, BotBye could not evaluate the request (e.g. invalid server key). In that case decision defaults to "ALLOW" so that a misconfiguration does not block real users — but you should monitor and fix the underlying error.
Examples of BotBye API responses
Blocked (bot detected):
1
2
3
4
5
6
7
{
"request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"decision": "BLOCK",
"risk_score": 0.95,
"scores": { "bot": 0.95 },
"signals": ["AutomationTool"]
}
Allowed:
1
2
3
4
5
6
7
{
"request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"decision": "ALLOW",
"risk_score": 0.05,
"scores": { "bot": 0.05, "ato": 0.02 },
"signals": []
}
Challenge:
1
2
3
4
5
6
7
8
{
"request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"decision": "CHALLENGE",
"risk_score": 0.65,
"scores": { "bot": 0.65 },
"signals": ["SuspiciousFingerprint"],
"challenge": { "type": "captcha", "token": "..." }
}
Invalid server-key:
1
2
3
4
{
"decision": "ALLOW",
"error": { "message": "[BotBye] Bad Request: Invalid Server Key" }
}
Middleware usage
BotByeMiddleware evaluates every incoming request automatically and stores the result on the request object. Use the @BotByeResponse() parameter decorator to access the result in a controller without calling evaluate manually.
Provide a tokenExtractor in module options so the middleware knows where to find the BotBye token:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app.module.ts
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common";
import { BotByeModule, BotByeMiddleware } from "@botbye/nest-fastify";
@Module({
imports: [
BotByeModule.register({
// Use your project server-key
serverKey: "00000000-0000-0000-0000-000000000000",
// "x-botbye-token" is an example — pass the token from wherever you store it
tokenExtractor: (req) => req.headers["x-botbye-token"] as string | undefined,
}),
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// Global: protect all routes
consumer.apply(BotByeMiddleware).forRoutes("*");
}
}
Access the result in a controller with @BotByeResponse():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app.controller.ts
import { Controller, Post, ForbiddenException } from "@nestjs/common";
import { BotByeResponse } from "@botbye/nest-fastify";
import type { TEvaluationResult } from "@botbye/nest-fastify";
@Controller()
export class AppController {
@Post("/api/submit")
async submit(@BotByeResponse() result: TEvaluationResult) {
if (result.decision === "BLOCK") {
throw new ForbiddenException();
}
// proceed normally
}
}
To apply middleware only to specific routes, replace .forRoutes("*") with a path or route descriptor:
1
2
// Scoped: protect only routes registered under "protected"
consumer.apply(BotByeMiddleware).forRoutes("protected");