CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-fastify--static

Plugin for serving static files as fast as possible.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

reply-decorators.mddocs/

Reply Decorators

Manual file serving methods added to FastifyReply for programmatic file operations with custom headers, download behavior, and flexible root path specification.

Capabilities

sendFile Method

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

download Method

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

Send Options

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

Error Handling

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

Advanced Usage Patterns

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

docs

directory-listing.md

index.md

plugin-registration.md

reply-decorators.md

tile.json