Plugin for serving static files as fast as possible.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Manual file serving methods added to FastifyReply for programmatic file operations with custom headers, download behavior, and flexible root path specification.
Send a file from the configured root directory or a custom root path with optional response customization.
/**
* Send a file from static root or custom root path
* @param filename - Name of file to send relative to root
* @param rootPath - Optional custom root path (string) or options object
* @param options - Optional send options to override defaults
* @returns FastifyReply instance for chaining
*/
sendFile(filename: string, rootPath?: string): FastifyReply;
sendFile(filename: string, options?: SendOptions): FastifyReply;
sendFile(filename: string, rootPath?: string, options?: SendOptions): FastifyReply;Usage Examples:
// Basic usage - serve from configured root
fastify.get('/file', (req, reply) => {
return reply.sendFile('document.pdf');
});
// Custom root path
fastify.get('/upload', (req, reply) => {
return reply.sendFile('image.jpg', path.join(__dirname, 'uploads'));
});
// With options to override defaults
fastify.get('/cached', (req, reply) => {
return reply.sendFile('asset.css', {
maxAge: '1d',
immutable: true,
cacheControl: true
});
});
// Custom root with options
fastify.get('/temp', (req, reply) => {
return reply.sendFile('temp.txt', '/tmp', {
maxAge: 0,
cacheControl: false
});
});
// Async handler pattern
fastify.get('/async-file', async (req, reply) => {
return reply.sendFile('data.json');
});Send a file with content-disposition header to trigger download behavior in browsers.
/**
* Send file with content-disposition header for download
* @param filepath - Path to file relative to root
* @param filename - Optional custom filename for download (string) or options object
* @param options - Optional send options to override defaults
* @returns FastifyReply instance for chaining
*/
download(filepath: string, options?: SendOptions): FastifyReply;
download(filepath: string, filename?: string): FastifyReply;
download(filepath: string, filename?: string, options?: SendOptions): FastifyReply;Usage Examples:
// Basic download - uses original filename
fastify.get('/download', (req, reply) => {
return reply.download('reports/annual-report.pdf');
});
// Custom download filename
fastify.get('/report', (req, reply) => {
return reply.download('reports/2023-annual.pdf', 'Annual Report 2023.pdf');
});
// Download with options
fastify.get('/export', (req, reply) => {
return reply.download('exports/data.csv', {
maxAge: 0, // Don't cache downloads
cacheControl: false
});
});
// Custom filename with options
fastify.get('/backup', (req, reply) => {
return reply.download(
'backups/db-backup.sql',
'database-backup.sql',
{
dotfiles: 'allow',
maxAge: 0
}
);
});
// Dynamic download based on parameters
fastify.get('/download/:type/:id', (req, reply) => {
const { type, id } = req.params;
const filename = `${type}-${id}.pdf`;
const downloadName = `${type.toUpperCase()}_${id}.pdf`;
return reply.download(filename, downloadName);
});Configuration options for customizing file sending behavior, overriding plugin defaults.
/**
* Options for customizing file sending behavior
*/
interface SendOptions {
/** Enable/disable accepting ranged requests */
acceptRanges?: boolean;
/** Enable/disable Content-Type header setting */
contentType?: boolean;
/** Enable/disable Cache-Control header */
cacheControl?: boolean;
/** How to handle dotfiles: 'allow' | 'deny' | 'ignore' */
dotfiles?: 'allow' | 'deny' | 'ignore';
/** Enable/disable etag generation */
etag?: boolean;
/** File extensions to attempt when no extension in URL */
extensions?: string[];
/** Enable immutable directive in Cache-Control */
immutable?: boolean;
/** Index file names or false to disable */
index?: string[] | string | false;
/** Enable/disable Last-Modified header */
lastModified?: boolean;
/** Cache max-age in ms or time string */
maxAge?: string | number;
}Usage Examples:
// Aggressive caching for assets
const assetOptions = {
maxAge: '1y',
immutable: true,
cacheControl: true,
etag: true
};
fastify.get('/assets/:file', (req, reply) => {
return reply.sendFile(req.params.file, assetOptions);
});
// No caching for dynamic content
const dynamicOptions = {
maxAge: 0,
cacheControl: false,
etag: false,
lastModified: false
};
fastify.get('/api/export', (req, reply) => {
return reply.download('temp/export.json', dynamicOptions);
});
// Security-focused options
const secureOptions = {
dotfiles: 'deny',
extensions: false, // Don't try extensions
index: false // Don't serve index files
};
fastify.get('/secure/:file', (req, reply) => {
return reply.sendFile(req.params.file, secureOptions);
});Handle errors during file serving operations.
Usage Examples:
// Handle file not found
fastify.get('/maybe-file', (req, reply) => {
try {
return reply.sendFile('might-not-exist.txt');
} catch (error) {
if (error.code === 'ENOENT') {
return reply.code(404).send({ error: 'File not found' });
}
throw error;
}
});
// With custom error handler
fastify.setErrorHandler((error, request, reply) => {
if (error.statusCode === 404) {
reply.code(404).sendFile('404.html');
} else {
reply.send(error);
}
});
// Conditional file serving
fastify.get('/conditional/:file', async (req, reply) => {
const { file } = req.params;
// Check if file exists before sending
try {
await fs.access(path.join(__dirname, 'public', file));
return reply.sendFile(file);
} catch {
return reply.code(404).send({ error: 'File not available' });
}
});Complex scenarios combining multiple features.
Usage Examples:
// Conditional root paths
fastify.get('/media/:type/:file', (req, reply) => {
const { type, file } = req.params;
const rootMap = {
'images': path.join(__dirname, 'images'),
'videos': path.join(__dirname, 'videos'),
'docs': path.join(__dirname, 'documents')
};
const root = rootMap[type];
if (!root) {
return reply.code(404).send({ error: 'Invalid media type' });
}
return reply.sendFile(file, root, {
maxAge: type === 'docs' ? '1h' : '1d'
});
});
// User-specific file serving
fastify.get('/user/:userId/files/:filename', async (req, reply) => {
const { userId, filename } = req.params;
// Verify user access
if (req.user.id !== userId && !req.user.isAdmin) {
return reply.code(403).send({ error: 'Access denied' });
}
const userRoot = path.join(__dirname, 'user-files', userId);
return reply.download(filename, `${req.user.name}-${filename}`, {
root: userRoot,
maxAge: 0
});
});
// Content-type override
fastify.get('/api-docs/:file', (req, reply) => {
const { file } = req.params;
if (file.endsWith('.json')) {
reply.type('application/json');
} else if (file.endsWith('.yaml')) {
reply.type('application/x-yaml');
}
return reply.sendFile(file, path.join(__dirname, 'api-docs'), {
contentType: false // Let us handle content-type manually
});
});