Fastify-based HTTP adapter for the NestJS framework, enabling high-performance HTTP server integration with NestJS applications
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Enhanced interfaces for NestJS applications with Fastify-specific functionality and configuration options for static assets, view engines, and body parsing.
Enhanced NestJS application interface that provides Fastify-specific methods and functionality.
/**
* Enhanced NestJS application interface with Fastify-specific methods
* Extends the standard INestApplication with Fastify adapter capabilities
*/
interface NestFastifyApplication<TServer extends RawServerBase = RawServerDefault>
extends INestApplication<TServer> {
/**
* Returns the underlying HTTP adapter bound to the Fastify app
*/
getHttpAdapter(): HttpServer<FastifyRequest, FastifyReply, FastifyInstance>;
/**
* Registers a Fastify plugin with the application
* Wrapper around the native fastify.register() method
* @param plugin - Fastify plugin (callback, async, or promise-wrapped)
* @param opts - Plugin registration options
*/
register<Options extends FastifyPluginOptions = any>(
plugin:
| FastifyPluginCallback<Options>
| FastifyPluginAsync<Options>
| Promise<{ default: FastifyPluginCallback<Options> }>
| Promise<{ default: FastifyPluginAsync<Options> }>,
opts?: FastifyRegisterOptions<Options>
): Promise<FastifyInstance>;
/**
* Registers custom body parsers on the fly
* Respects the application's rawBody option
* @param type - Content type(s) to parse
* @param options - Body parser options
* @param parser - Custom parser function
*/
useBodyParser<TServer extends RawServerBase = RawServerBase>(
type: string | string[] | RegExp,
options?: NestFastifyBodyParserOptions,
parser?: FastifyBodyParser<Buffer, TServer>
): this;
/**
* Configures static asset serving
* @param options - Static asset configuration options
*/
useStaticAssets(options: FastifyStaticOptions): this;
/**
* Enables Cross-Origin Resource Sharing (CORS)
* @param options - CORS configuration options
*/
enableCors(options?: FastifyCorsOptions): void;
/**
* Sets up a view engine for template rendering
* @param options - View engine configuration (object only, string will cause error)
*/
setViewEngine(options: FastifyViewOptions | string): this;
/**
* Injects a test request for testing purposes
* Returns a chain for building the request
*/
inject(): LightMyRequestChain;
/**
* Injects a test request with specific options
* @param opts - Request options or URL string
*/
inject(opts: InjectOptions | string): Promise<LightMyRequestResponse>;
/**
* Starts the application server
* Multiple overloads for different parameter combinations
*/
listen(
opts: FastifyListenOptions,
callback?: (err: Error | null, address: string) => void
): Promise<TServer>;
listen(opts?: FastifyListenOptions): Promise<TServer>;
listen(callback?: (err: Error | null, address: string) => void): Promise<TServer>;
listen(
port: number | string,
callback?: (err: Error | null, address: string) => void
): Promise<TServer>;
listen(
port: number | string,
address: string,
callback?: (err: Error | null, address: string) => void
): Promise<TServer>;
listen(
port: number | string,
address: string,
backlog: number,
callback?: (err: Error | null, address: string) => void
): Promise<TServer>;
}Usage Examples:
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
import * as path from 'path';
async function bootstrap() {
// Create application with proper typing
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter()
);
// Register Fastify plugins
await app.register(require('@fastify/helmet'));
await app.register(require('@fastify/rate-limit'), {
max: 100,
timeWindow: '1 minute'
});
// Configure body parsing
app.useBodyParser('application/xml', { bodyLimit: 1024 * 1024 });
// Setup static assets
app.useStaticAssets({
root: path.join(__dirname, '..', 'public'),
prefix: '/static/',
});
// Enable CORS
app.enableCors({
origin: ['http://localhost:3000', 'https://example.com'],
credentials: true
});
// Setup view engine
app.setViewEngine({
engine: {
handlebars: require('handlebars')
},
templates: path.join(__dirname, '..', 'views'),
options: {
partials: {
header: 'partials/header.hbs',
footer: 'partials/footer.hbs'
}
}
});
// Start server
await app.listen({
port: 3000,
host: '0.0.0.0'
});
}Type definition for configuring custom body parsers.
/**
* Options for configuring Fastify body parsers
* Excludes 'parseAs' property which is handled internally
*/
type NestFastifyBodyParserOptions = Omit<
Parameters<AddContentTypeParser>[1],
'parseAs'
>;Common body parser options include:
interface BodyParserOptions {
/** Maximum body size in bytes */
bodyLimit?: number;
/** Parser-specific configuration */
parseAs?: 'string' | 'buffer';
/** Content type parser configuration */
contentTypeParser?: {
// Parser implementation details
};
}Usage Examples:
// Register XML body parser
app.useBodyParser('application/xml', {
bodyLimit: 2 * 1024 * 1024, // 2MB limit
}, (req, body, done) => {
// Custom XML parsing logic
const xmlParser = require('fast-xml-parser');
try {
const parsed = xmlParser.parse(body.toString());
done(null, parsed);
} catch (error) {
done(error);
}
});
// Register multiple content types
app.useBodyParser(['text/plain', 'text/html'], {
bodyLimit: 1024 * 1024, // 1MB limit
}, (req, body, done) => {
done(null, body.toString());
});Configuration interface for serving static files through Fastify.
/**
* Configuration options for static asset serving
* Based on @fastify/static plugin options
*/
interface FastifyStaticOptions extends SendOptions {
/** Root directory or directories for static files */
root: string | string[] | URL | URL[];
/** URL prefix for serving static files */
prefix?: string;
/** Avoid trailing slash in prefix */
prefixAvoidTrailingSlash?: boolean;
/** Whether to serve files (can be disabled for decorateReply only) */
serve?: boolean;
/** Add reply.sendFile method to reply object */
decorateReply?: boolean;
/** Hide from OpenAPI/JSON schema */
schemaHide?: boolean;
/** Function to set custom headers */
setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void;
/** Enable redirect from folder to folder/ */
redirect?: boolean;
/** Enable wildcard matching */
wildcard?: boolean;
/** Enable/configure directory listing */
list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
/** Function to filter allowed paths */
allowedPath?: (
pathName: string,
root: string,
request: FastifyRequest
) => boolean;
/** Look for pre-compressed files (.gz, .br) */
preCompressed?: boolean;
/** Route constraints for static serving */
constraints?: RouteOptions['constraints'];
// Inherited from SendOptions
acceptRanges?: boolean;
cacheControl?: boolean;
dotfiles?: 'allow' | 'deny' | 'ignore';
etag?: boolean;
extensions?: string[];
immutable?: boolean;
index?: string[] | string | false;
lastModified?: boolean;
maxAge?: string | number;
}
interface SetHeadersResponse {
getHeader: FastifyReply['getHeader'];
setHeader: FastifyReply['header'];
readonly filename: string;
statusCode: number;
}
interface ListOptionsJsonFormat {
format: 'json';
names: string[];
extendedFolderInfo?: boolean;
jsonFormat?: 'names' | 'extended';
render?: ListRender;
}
interface ListOptionsHtmlFormat {
format: 'html';
names: string[];
extendedFolderInfo?: boolean;
jsonFormat?: 'names' | 'extended';
render: ListRender;
}
interface ListRender {
(dirs: ListDir[], files: ListFile[]): string;
}Usage Examples:
// Basic static assets
app.useStaticAssets({
root: path.join(__dirname, '..', 'public'),
prefix: '/static/',
});
// Advanced configuration
app.useStaticAssets({
root: [
path.join(__dirname, '..', 'public'),
path.join(__dirname, '..', 'uploads')
],
prefix: '/assets/',
maxAge: '1d',
etag: true,
lastModified: true,
immutable: true,
preCompressed: true,
setHeaders: (res, path, stat) => {
if (path.endsWith('.pdf')) {
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Security-Policy', "default-src 'self'");
}
},
allowedPath: (pathName, root, request) => {
// Only allow access to certain files based on user permissions
return !pathName.includes('private') || request.headers.authorization;
},
list: {
format: 'json',
names: ['index.html'],
extendedFolderInfo: true
}
});
// Serving with constraints
app.useStaticAssets({
root: path.join(__dirname, '..', 'admin-assets'),
prefix: '/admin/',
constraints: {
host: 'admin.example.com'
}
});Configuration interface for template engines and view rendering.
/**
* Configuration options for view engine setup
* Based on @fastify/view plugin options
*/
interface FastifyViewOptions {
/** Template engine configuration */
engine: {
/** EJS template engine */
ejs?: any;
/** Eta template engine */
eta?: any;
/** Nunjucks template engine */
nunjucks?: any;
/** Pug template engine */
pug?: any;
/** Handlebars template engine */
handlebars?: any;
/** Mustache template engine */
mustache?: any;
/** Art Template engine */
'art-template'?: any;
/** Twig template engine */
twig?: any;
/** Liquid template engine */
liquid?: any;
/** doT template engine */
dot?: any;
};
/** Template directory path(s) */
templates?: string | string[];
/** Include file extension in template names */
includeViewExtension?: boolean;
/** Engine-specific options */
options?: object;
/** Character encoding for templates */
charset?: string;
/** Maximum number of cached templates */
maxCache?: number;
/** Production mode optimizations */
production?: boolean;
/** Default context for all templates */
defaultContext?: object;
/** Default layout template */
layout?: string;
/** Root directory for templates */
root?: string;
/** Default view file extension */
viewExt?: string;
/** Property name for reply.view method */
propertyName?: string;
/** Property name for async reply method */
asyncProperyName?: string;
}Usage Examples:
// Handlebars setup
app.setViewEngine({
engine: {
handlebars: require('handlebars')
},
templates: path.join(__dirname, '..', 'views'),
options: {
partials: {
header: 'partials/header.hbs',
footer: 'partials/footer.hbs',
sidebar: 'partials/sidebar.hbs'
},
helpers: {
formatDate: (date) => new Date(date).toLocaleDateString(),
uppercase: (str) => str.toUpperCase()
}
},
layout: 'layouts/main.hbs',
viewExt: 'hbs',
defaultContext: {
siteName: 'My Application',
year: new Date().getFullYear()
}
});
// Pug setup with production optimizations
app.setViewEngine({
engine: {
pug: require('pug')
},
templates: path.join(__dirname, '..', 'views'),
production: process.env.NODE_ENV === 'production',
maxCache: 100,
options: {
pretty: process.env.NODE_ENV !== 'production',
cache: process.env.NODE_ENV === 'production',
basedir: path.join(__dirname, '..', 'views')
}
});
// Multiple template directories
app.setViewEngine({
engine: {
ejs: require('ejs')
},
templates: [
path.join(__dirname, '..', 'views'),
path.join(__dirname, '..', 'shared-templates')
],
options: {
delimiter: '?',
openDelimiter: '<',
closeDelimiter: '>',
cache: true
}
});Interfaces for testing with light-my-request integration.
/**
* Options for injecting test requests
*/
interface InjectOptions {
method?: string;
url?: string;
path?: string;
query?: string | Record<string, any>;
payload?: any;
headers?: Record<string, string>;
cookies?: Record<string, string>;
remoteAddress?: string;
server?: any;
simulate?: {
end?: boolean;
split?: boolean;
error?: boolean;
close?: boolean;
};
validate?: boolean;
authority?: string;
}
/**
* Chain interface for building test requests
*/
interface LightMyRequestChain {
get(url: string): LightMyRequestChain;
post(url: string): LightMyRequestChain;
put(url: string): LightMyRequestChain;
patch(url: string): LightMyRequestChain;
delete(url: string): LightMyRequestChain;
head(url: string): LightMyRequestChain;
options(url: string): LightMyRequestChain;
headers(headers: Record<string, string>): LightMyRequestChain;
payload(payload: any): LightMyRequestChain;
query(query: Record<string, any>): LightMyRequestChain;
cookies(cookies: Record<string, string>): LightMyRequestChain;
end(): Promise<LightMyRequestResponse>;
}
/**
* Response from injected test requests
*/
interface LightMyRequestResponse {
statusCode: number;
statusMessage: string;
headers: Record<string, string>;
rawPayload: Buffer;
payload: string;
body: string;
json(): any;
cookies: Array<{
name: string;
value: string;
path?: string;
domain?: string;
expires?: Date;
httpOnly?: boolean;
secure?: boolean;
sameSite?: string;
}>;
}Usage Examples:
// In test files
describe('ProductsController', () => {
let app: NestFastifyApplication;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [ProductsController],
}).compile();
app = module.createNestApplication<NestFastifyApplication>(
new FastifyAdapter()
);
await app.init();
});
it('should return products', async () => {
const response = await app.inject({
method: 'GET',
url: '/products',
query: { page: '1', limit: '10' }
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
products: expect.any(Array),
total: expect.any(Number)
});
});
it('should create product', async () => {
const productData = {
name: 'Test Product',
price: 29.99,
category: 'electronics'
};
const response = await app.inject()
.post('/products')
.headers({ 'content-type': 'application/json' })
.payload(productData)
.end();
expect(response.statusCode).toBe(201);
expect(response.json()).toMatchObject(productData);
});
});