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
});
});