Skip to content

Arcjet Fastify SDK reference

npm badge

This guide shows how to use the package @arcjet/fastify. Its source code is on GitHub. The code is open source and licensed under Apache 2.0.

What is Arcjet? Arcjet helps developers protect their apps in just a few lines of code. Bot detection. Rate limiting. Email validation. Attack protection. Data redaction. A developer-first approach to security.

See the Fastify quick start.

  • Fastify 5 or later
  • Node.js 20 or later, or similar runtime
  • ESM

Build Arcjet clients as few times as possible. That means outside request handlers. If you need different strategies, such as one for logged-in users and one for guests, create two clients and choose which one to use inside the handler.

The main way to configure Arcjet is to pass options to the arcjet function. The fields are:

  • characteristics (Array<string>, default: ["src.ip"]) — characteristics to track a user by; can also be passed to rules
  • client (Client, optional) — client used to make requests to the Cloud API
  • key (string, required) — API key to identify the site in Arcjet (typically through process.env.ARCJET_KEY)
  • log (ArcjetLogger, optional) — log interface to emit useful info
  • rules (Array<ArcjetRule>, required) — rules to use (order insensitive)

Get your site key from the Arcjet dashboard. Set it as an environment variable called ARCJET_KEY in your .env file:

Terminal window
ARCJET_KEY=your_site_key_here

Next to the convention of using ARCJET_KEY for the Arcjet Cloud API key, there are several environment variables that affect how Arcjet works.

  • ARCJET_BASE_URL — an allowed URL to the Cloud API (defaults to https://decide.arcjet.com)
  • ARCJET_ENV, MODE, NODE_ENV — whether development mode is on
  • ARCJET_LOG_LEVEL — log level to use
  • FLY_APP_NAME, VERCEL, RENDER — hosting platform

Use the protect function to protect a request from Fastify. Some rules, such as validateEmail, may need extra properties. The protect function returns a promise that resolves to a decision.

import arcjetFastify, { tokenBucket } from "@arcjet/fastify";
import Fastify from "fastify";
const arcjetKey = process.env.ARCJET_KEY;
if (!arcjetKey) {
throw new Error("Cannot find `ARCJET_KEY` environment variable");
}
const arcjet = arcjetFastify({
key: arcjetKey,
rules: [
tokenBucket({
capacity: 10,
characteristics: ["userId"],
interval: 10,
mode: "LIVE",
refillRate: 5,
}),
],
});
const fastify = Fastify({ logger: true });
fastify.get("/", async function (request, reply) {
// Replace `userId` with your authenticated user ID.
const userId = "user123";
const decision = await arcjet.protect(request, {
requested: 5,
userId,
});
if (decision.isDenied()) {
return reply.status(403).send("Forbidden");
}
return reply.status(200).send("Hello world");
});
await fastify.listen({ port: 3000 });

The ArcjetDecision that protect resolves to has the following fields:

  • conclusion ("ALLOW", "DENY", or "ERROR") — what to do with the request
  • id (string) — ID for the request; local decisions start with lreq_ and remote ones with req_
  • ip (ArcjetIpDetails) — analysis of the client IP address
  • reason (ArcjetReason) — more info about the conclusion
  • results (Array<ArcjetRuleResult>) — results of each rule
  • ttl (number) — time-to-live for the decision in seconds; "DENY" decisions are cached by @arcjet/fastify for this duration

This top-level decision takes the results from each "LIVE" rule into account. If one of them is "DENY" then the overall conclusion will be "DENY". Otherwise, if one of them is "ERROR", then "ERROR". Otherwise, it will be "ALLOW". The reason and ttl fields reflect this conclusion.

To illustrate, when a bot rule returns an error and a validate email rule returns a deny, the overall conclusion is "DENY", while the "ERROR" is available in the results.

The results of "DRY_RUN" rules do not affect this overall decision, but are included in results.

The ip field is available when the Cloud API was called and contains IP geolocation and reputation info. You can use this field to customize responses or you can use filter rules to make decisions based on it. See the IP geolocation and IP reputation blueprints for more info.

Arcjet fails open so that a service issue, misconfiguration, or network timeout does not block requests. Such errors should in many cases be logged but otherwise treated as "ALLOW" decisions. The reason.message field has more info on what occured.

import arcjetFastify, { filter } from "@arcjet/fastify";
import Fastify from "fastify";
const arcjetKey = process.env.ARCJET_KEY;
if (!arcjetKey) {
throw new Error("Cannot find `ARCJET_KEY` environment variable");
}
const arcjet = arcjetFastify({
key: arcjetKey,
rules: [
// This broken expression will result in an error decision:
filter({ deny: ['ip.src.country is "'] }),
],
});
const fastify = Fastify({ logger: true });
fastify.get("/", async function (request, reply) {
const decision = await arcjet.protect(request);
if (decision.isErrored()) {
console.warn("Arcjet error", decision.reason.message);
}
if (decision.isDenied()) {
return reply.status(403).send("Forbidden");
}
return reply.status(200).send("Hello world");
});
await fastify.listen({ port: 3000 });

You can use a custom log interface matching pino to change the default behavior. Using pino-pretty as an example:

Then, create a custom logger that will log to JSON in production and pretty print in development:

import arcjetFastify from "@arcjet/fastify";
import pino from "pino";
const arcjetKey = process.env.ARCJET_KEY;
if (!arcjetKey) {
throw new Error("Cannot find `ARCJET_KEY` environment variable");
}
const arcjet = arcjetFastify({
key: arcjetKey,
log: pino({
// Warn in development, debug otherwise.
level:
process.env.ARCJET_LOG_LEVEL ||
(process.env.ARCJET_ENV === "development" ? "debug" : "warn"),
// Pretty print in development, JSON otherwise.
transport:
process.env.ARCJET_ENV === "development"
? { options: { colorize: true }, target: "pino-pretty" }
: undefined,
}),
rules: [
// …
],
});

You can pass a client to change the behavior when connecting to the Cloud API. Use createRemoteClient to create a client.

import arcjetFastify, { createRemoteClient } from "@arcjet/fastify";
const arcjetKey = process.env.ARCJET_KEY;
if (!arcjetKey) {
throw new Error("Cannot find `ARCJET_KEY` environment variable");
}
const arcjet = arcjetFastify({
key: arcjetKey,
client: createRemoteClient({ timeout: 3000 }),
rules: [
// …
],
});

Discussion