Skip to content

Protecting Routes with Guards

See Guards & Authorization for the concept; this walks through wiring one up end to end.

src/auth/api-key.guard.ts
import { createHash, timingSafeEqual } from "node:crypto";
import type { CanActivate, ExecutionContext } from "@blixis-io/http";
import { Injectable } from "@blixis-io/di";
const digest = (value: string) => createHash("sha256").update(value).digest();
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const expected = process.env["API_KEY"];
const given = context.request.headers.get("x-api-key");
// Compare digests in constant time, so the response time does not reveal how much of a guess was right.
return expected !== undefined && given !== null && timingSafeEqual(digest(given), digest(expected));
}
}

Guard classes are resolved through DI at request time — @UseGuards alone doesn’t register them (see Guards & Authorization):

src/posts/posts.module.ts
@Module({
providers: [PostService, ApiKeyGuard],
controllers: [PostController],
})
export class PostsModule {}
src/posts/posts.controller.ts
import { UseGuards } from "@blixis-io/http";
import { ApiKeyGuard } from "../auth/api-key.guard.js";
@Controller("posts")
export class PostController {
@Get()
list() { /* open to everyone */ }
@UseGuards(ApiKeyGuard)
@Delete(":id")
remove(@Param("id") id: string) { /* requires the header */ }
}

Put @UseGuards on the @Controller() class itself instead to protect every route in it.

import { Test } from "@blixis-io/testing";
it("denies without the key and allows with it", async () => {
const app = await Test.createModule({ imports: [PostsModule] }).compile();
const denied = await app.request("/posts/1", { method: "DELETE" });
expect(denied.status).toBe(403);
const allowed = await app.request("/posts/1", {
method: "DELETE",
headers: { "x-api-key": "dev-secret" },
});
expect(allowed.status).toBe(204);
await app.close();
});

Guards are ordinary providers, so constructor injection works exactly the same way — a real auth guard almost always needs one:

@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly users: UserService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const token = context.request.headers.get("authorization");
return token !== null && (await this.users.verifyToken(token));
}
}

For the full walkthrough building this exact pattern into a real app, see Add Authentication.