NestJS
First-class NestJS integration - a dynamic FilesModule that mounts the gateway, plus InjectFiles() for sharing the Files instance through DI.
files-sdk/nestjs wraps the gateway in an idiomatic NestJS module. FilesModule.forRoot() (or forRootAsync()) configures createFilesRouter, mounts it at a configurable path via Nest’s middleware layer, and exposes the Files instance through dependency injection — inject it anywhere with @InjectFiles(). Both the Express and Fastify platform adapters are supported.
import { Module } from "@nestjs/common";
import { createFiles } from "files-sdk";
import { FilesModule } from "files-sdk/nestjs";
import { r2 } from "files-sdk/r2";
@Module({
imports: [
FilesModule.forRoot({
files: createFiles({ adapter: r2({ bucket: "uploads" }) }),
path: "/api/files", // the default
allowedOrigins: ["https://app.example.com"],
operations: ["upload", "download", "list", "delete", "url"],
secret: process.env.FILES_API_SECRET,
}),
],
})
export class AppModule {}
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
// Express adapter: disable the global body parser (see below).
const app = await NestFactory.create(AppModule, { bodyParser: false });
await app.listen(3000);
CSRF protection and security headers
NestJS 12.1’s opt-in app.enableCsrfProtection() and app.useSecurityHeaders() both run before Nest middleware, so they apply to the gateway too. The CSRF check rejects every cross-origin POST and PUT, so list the same origins you pass to allowedOrigins in its trustedOrigins. The security headers default to Cross-Origin-Resource-Policy: same-origin, which blocks other origins from embedding proxied downloads; relax it if they do.
const app = await NestFactory.create(AppModule, { bodyParser: false });
app.enableCsrfProtection({ trustedOrigins: ["https://app.example.com"] });
// Only if another origin embeds proxied downloads (an <img>, a PDF viewer):
app.useSecurityHeaders({
crossOriginResourcePolicy: { policy: "cross-origin" },
});
await app.listen(3000);
Async configuration
forRootAsync() follows the standard Nest pattern — imports, inject, and a useFactory that returns the same options:
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { createFiles } from "files-sdk";
import { FilesModule } from "files-sdk/nestjs";
import { r2 } from "files-sdk/r2";
@Module({
imports: [
ConfigModule.forRoot(),
FilesModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
files: createFiles({
adapter: r2({ bucket: config.getOrThrow("R2_BUCKET") }),
}),
secret: config.getOrThrow("FILES_API_SECRET"),
allowedOrigins: [config.getOrThrow("APP_ORIGIN")],
operations: ["upload", "download", "list", "delete", "url"],
}),
}),
],
})
export class AppModule {}
Injecting the Files instance
The module registers globally by default (global: false to opt out), so any provider can inject the shared instance without importing FilesModule:
import { Injectable } from "@nestjs/common";
import type { Files } from "files-sdk";
import { InjectFiles } from "files-sdk/nestjs";
@Injectable()
export class UploadsService {
constructor(@InjectFiles() private readonly files: Files) {}
async uploadAvatar(userId: string, file: Blob) {
return await this.files.upload(`avatars/${userId}.png`, file, {
contentType: file.type,
});
}
}
The configured gateway router is also available under the FILES_API token for manual wiring or tests.
Options beyond path and global are the gateway options (minus the per-request files factory form — mount files-sdk/express manually for multi-tenant routing), and the endpoint speaks the same protocol as every other binding: point useFiles or createFilesClient at your mount path and lock it down with authorize.