UNPKG

fastify-fusion

Version:

Fastify API framework with `best practices` and `plugins` fused together to make it easy to build and maintain your API.

559 lines (441 loc) 17.6 kB
![Fastify Fusion](https://jaredwray.com/images/fastify-fusion.svg) # fastify-fusion [![codecov](https://codecov.io/gh/jaredwray/fastify-fusion/graph/badge.svg?token=ieUorXA15v)](https://codecov.io/gh/jaredwray/fastify-fusion) [![tests](https://github.com/jaredwray/fastify-fusion/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/fastify-fusion/actions/workflows/tests.yaml) [![npm](https://img.shields.io/npm/v/fastify-fusion)](https://www.npmjs.com/package/fastify-fusion) [![npm](https://img.shields.io/npm/dm/fastify-fusion)](https://www.npmjs.com/package/fastify-fusion) [![license](https://img.shields.io/github/license/jaredwray/fastify-fusion)](https://github.com/jaredwray/fastify-fusion/blob/main/LICENSE) Fastify API framework with `best practices` and `plugins` fused together to make it easy to build and maintain your API. # Features - **Fuse** - Easily create a Fastify app with sensible defaults via `fuse()`. - **Start** - Easy to start your Fastify app with sensible defaults via `start()`. - **Cache** - High-performance layer 1/2 caching with the `cacheable` library integrated as `app.cache`. - **OpenAPI** - OpenAPI docs generated using `fastify-swagger` and `scalar` with sensible defaults. - **Helmet** - Security headers set using `@fastify/helmet` with sensible defaults. - **CORS** - CORS enabled using `@fastify/cors` with sensible defaults for apis. - **Logging** - Pino Configured using `pino-pretty` to make it easy to read and access to a `logger` instance. - **Rate Limiting** - Rate limiting using `@fastify/rate-limit` with sensible defaults. - **Static Paths**: Default `./public` static path and easy to add / configure your own. - **Regularly updated**: Updated regularly to keep up with the latest Fastify and TypeScript features. # Table of Contents - [Installation](#installation) - [Usage](#usage) - [Fuse Options](#fuse-options) - [Fastify Start](#fastify-start) - [Cache](#cache) - [Static Paths](#static-paths) - [Logging](#logging) - [Helmet](#helmet) - [Rate Limiting](#rate-limiting) - [CORS](#cors) - [Open API and Docs UX](#open-api-and-docs-ux) - [How to Contribute](#how-to-contribute) - [Licensing and Copyright](#licensing-and-copyright) # Installation ```bash npm install fastify-fusion fastify ``` # Usage If you already have a Fastify app, you can use `fuse` to add the default options and plugins to your app. ```typescript import fastify from 'fastify'; import { fuse, FuseOptions } from 'fastify-fusion'; const app = fastify(); // Fuse the app. It will use the default options if none are provided. If you want to use your own options, pass them in as the second argument. await fuse(app); ``` You can also pass in the `FuseOptions` to customize your fastify instance. ```typescript import fastify from 'fastify'; import { fuse, FuseOptions } from 'fastify-fusion'; const fuseOptions: FuseOptions = { static: true, log: true, helmet: false, rateLimit: false, cache: true }; const app = await fuse(app, fuseOptions); ``` You can also use the built in `start()` function to get up and running quickly. This function will create a Fastify app and start the server for you. ```typescript import fastify from 'fastify'; import { start, fuse } from 'fastify-fusion'; const app = fastify(); // fuse the app with default options await fuse(app); // start the app. Set the options with StartOptions type. await start(app); ``` # Fuse Options You can customize the behavior of `fastify-fusion` by passing in options to the `fuse` function or when creating a new Fastify app with `fastify()`. ```typescript import { fuse, FuseOptions } from 'fastify-fusion'; import Fastify from 'fastify'; const app = Fastify(); const options: FuseOptions = { helmet: { contentSecurityPolicy: false, // Disable CSP for simplicity }, static: { path: '/static/', // Serve static files from /public dir: './static', // Path to the static files }, rateLimit: { max: 200, // Allow 200 requests per minute per IP address }, }; await fuse(app, options); ``` Here is the `FuseOptions` interface with all the available options: ```typescript export type FuseOptions = { static?: boolean | StaticOptions; log?: boolean | LoggerOptions; helmet?: boolean | FastifyHelmetOptions; rateLimit?: boolean | FastifyRateLimitOptions; cors?: boolean | FastifyCorsOptions; openApi?: boolean | OpenApiOptions; cache?: boolean | CacheableOptions; }; ``` By default, all the options are set to `true`, which means that all of the default settings will be applied. You can learn about the default settings in each features's documentation below. # Fastify Start You can start your Fastify app using the `start` function. This function will start the Fastify server and log the URL where the server is running. This will use the default configuration for the server, which includes a default port of `3000` and a host of `0.0.0.0` if `process.env.PORT` or `process.env.HOST` are not set. ```typescript import { fastify, start } from 'fastify-fusion'; const app = await fastify(); start(app); ``` You can customize the port and host by passing in a `StartOptions` object to the `start` function. ```typescript import { fastify, start, type StartOptions } from 'fastify-fusion'; const app = await fastify(); const options: StartOptions = { port: 3001, // Set the port to 3000 host: '127.0.0.1', // Set the host to 127.0.0.1 }; start(app, options); ``` If you want to also set the startup message when the server starts, you can pass in a `message` function to the `StartOptions`. This function will receive the host and port as arguments and should return a string that will be logged to the console. ```typescript import { fastify, start, type StartOptions } from 'fastify-fusion'; const app = await fastify(); const options: StartOptions = { message: (host, port) => `🌏 started successfully at http://${host}:${port}`, }; start(app, options); ``` # Cache `fastify-fusion` integrates the high-performance `cacheable` library as `app.cache`, providing layer 1/2 caching capabilities with advanced features like statistics, TTL management, and non-blocking operations. ## Default Configuration ```typescript export const defaultCacheableOptions: CacheableOptions = { ttl: "1h", // Default 1 hour TTL stats: true, // Enable statistics by default nonBlocking: true, // Non-blocking secondary operations }; ``` ## Basic Usage Once fused, the cache is available on your Fastify instance as `app.cache`: ```typescript import fastify from 'fastify'; import { fuse } from 'fastify-fusion'; const app = fastify(); await fuse(app); // Use cache in routes app.get('/user/:id', async (request, reply) => { const userId = request.params.id; const cacheKey = `user:${userId}`; // Try to get from cache first let user = await request.server.cache.get(cacheKey); if (!user) { // Fetch from database user = await getUserFromDatabase(userId); // Cache for 10 minutes await request.server.cache.set(cacheKey, user, '10m'); } return user; }); ``` ## Configuration Options You can customize the cache behavior by passing options to the `fuse` function: ```typescript import { fuse, FuseOptions } from 'fastify-fusion'; import fastify from 'fastify'; const app = fastify(); const options: FuseOptions = { cache: { ttl: '30m', // Default TTL of 30 minutes stats: true, // Enable cache statistics nonBlocking: false, // Blocking secondary operations }, }; await fuse(app, options); ``` You can also disable caching entirely: ```typescript const options: FuseOptions = { cache: false, // Disable caching }; ``` ## Cache Operations The cache instance provides all the standard caching operations: ```typescript // Basic get/set await app.cache.set('key', 'value', '1h'); const value = await app.cache.get('key'); // Bulk operations await app.cache.setMany([ { key: 'key1', value: 'value1' }, { key: 'key2', value: 'value2', ttl: '30m' } ]); const values = await app.cache.getMany(['key1', 'key2']); // Check existence const exists = await app.cache.has('key'); // Delete operations await app.cache.delete('key'); await app.cache.clear(); // Clear all // Take (get and delete) const takenValue = await app.cache.take('key'); ``` ## Advanced Features ### Memoization with wrap() Automatically cache function results: ```typescript const expensiveFunction = app.cache.wrap(async (input) => { // Expensive computation return await processData(input); }, { ttl: '1h' }); // First call computes and caches const result1 = await expensiveFunction('test'); // Second call returns cached result const result2 = await expensiveFunction('test'); ``` ### GetOrSet Pattern Fetch from cache or compute and store: ```typescript const posts = await app.cache.getOrSet('all-posts', async () => { return await fetchPostsFromAPI(); }, { ttl: '5m' }); ``` ### Cache Statistics Monitor cache performance when stats are enabled: ```typescript app.get('/cache/stats', async (request, reply) => { return { hits: app.cache.stats.hits, misses: app.cache.stats.misses, gets: app.cache.stats.gets, sets: app.cache.stats.sets, count: app.cache.stats.count }; }); ``` ### TTL Formats The cache supports both milliseconds and human-readable time formats: ```typescript // Milliseconds await app.cache.set('key', 'value', 3600000); // 1 hour // Human-readable await app.cache.set('key', 'value', '1h'); // 1 hour await app.cache.set('key', 'value', '30m'); // 30 minutes await app.cache.set('key', 'value', '15s'); // 15 seconds ``` The cache automatically handles cleanup and provides graceful shutdown when your Fastify server closes. # Static Paths By default `fastify-fusion` serves static files from the `./public` directory. You can change this by passing in a `StaticOptions` object to the `fuse` function. The default configuration serves static files from the `/public` path. Here is an example of how to customize the static file serving: ```typescript const defaultStaticPath = [ { dir: path.resolve('./public'), path: '/', }, ]; ``` ```typescript import { fuse, FuseOptions } from 'fastify-fusion'; import Fastify from 'fastify'; const app = Fastify(); const options: FuseOptions = { static: { dir: './static/', // Serve static files from /static path: '/static', // Path to the static files }, }; await fuse(app, options); ``` # Logging By default, `fastify-fusion` uses Pino for logging and configures it with sensible defaults. You can customize the logging behavior by passing in a `LoggerOptions` object to the `fuse` function. The default logging configuration uses `pino-pretty` and here are the default options: ```typescript export const defaultLoggingOptions = { transport: { target: 'pino-pretty', options: { colorize: true, translateTime: true, ignore: 'pid,hostname', singleLine: true, }, }, }; ``` Here is an example of how to customize the logging options: ```typescript import { fuse, FuseOptions } from 'fastify-fusion'; import Fastify from 'fastify'; const app = Fastify(); const options: FuseOptions = { log: { level: 'info', // Set the log level prettyPrint: true, // Enable pretty print for development }, }; await fuse(app, options); ``` If you want to access the logger instance, you can use the `logger` function. This function will return a logger instance that you can use to log messages in your application. This will use the default logging options if none are provided. Here is an example of how to use the logger instance: ```typescript import { logger } from 'fastify-fusion'; const log = logger(); log.info('This is an info message'); ``` # Helmet `fastify-fusion` uses `fastify-helmet` to set security headers by default. You can customize the behavior of `fastify-helmet` by passing in a `FastifyHelmetOptions` object to the `fuse` function. The default configuration sets the following headers: ```typescript export const defaultFastifyHelmetOptions: FastifyHelmetOptions = { // Turn off CSP (mostly for HTML) to avoid overhead contentSecurityPolicy: false, // Remove the X-Power-By header hidePoweredBy: true, // Prevent your API from being framed frameguard: {action: 'deny'}, // Disable DNS prefetching dnsPrefetchControl: {allow: false}, // Enable HSTS for one year on HTTPS endpoints hsts: { maxAge: 31_536_000, // 365 days in seconds includeSubDomains: true, preload: true, }, // Block sniffing of MIME types noSniff: true, // Basic XSS protections xssFilter: true, // Don't send Referer at all referrerPolicy: {policy: 'no-referrer'}, // Tighten cross-origin resource loading crossOriginResourcePolicy: {policy: 'same-origin'}, // You generally don't need the embedder/policy on an API crossOriginEmbedderPolicy: false, // Leave CSP nonces off // eslint-disable-next-line @typescript-eslint/naming-convention enableCSPNonces: false, }; ``` You can customize the security headers by passing in a `FastifyHelmetOptions` object to the `fuse` function. The default configuration sets the following: ```typescript import { fuse, FuseOptions } from 'fastify-fusion'; import Fastify from 'fastify'; const app = Fastify(); const options: FuseOptions = { helmet: { contentSecurityPolicy: false, // Disable CSP for simplicity crossOriginEmbedderPolicy: false, // Disable COEP for simplicity }, }; await fuse(app, options); ``` # Rate Limiting `fastify-fusion` uses `@fastify/rate-limit` to limit the number of requests to your API. By default, it allows 100 requests per minute per IP address. You can customize the rate limiting behavior by passing in a `RateLimitOptions` object to the `fuse` function. Here is an example of how to customize the rate limiting options: ```typescript export const defaultFastifyRateLimitOptions: FastifyRateLimitOptions = { // Enable rate limiting global: true, // Limit to 100 requests per minute max: 500, // Time window for the rate limit timeWindow: 60_000, // 1 minute in milliseconds // allow list for local development and testing allowList: ['127.0.0.1', '0.0.0.0'], }; ``` You can customize the rate limiting options by passing in a `RateLimitOptions` object to the `fuse` function. Here is an example of how to customize the rate limiting options: ```typescript import { fuse, FuseOptions } from 'fastify-fusion'; import Fastify from 'fastify'; const app = Fastify(); const options: FuseOptions = { rateLimit: { max: 200, // Allow 200 requests per minute per IP address }, }; await fuse(app, options); ``` # CORS `fastify-fusion` uses `@fastify/cors` to enable CORS for your API. By default, it allows all origins and methods. You can customize the CORS behavior by passing in a `FastifyCorsOptions` object to the `fuse` function. Here is an example of how to customize the CORS options: ```typescript import { fuse, FuseOptions } from 'fastify-fusion'; import Fastify from 'fastify'; const app = Fastify(); const options: FuseOptions = { cors: { origin: [ 'https://app.yourdomain.com', 'https://staging.yourdomain.com' ], methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'], // Allowed methods allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Bearer'], // Allowed headers exposedHeaders: ['Content-Length', 'X-Requested-With'], // Exposed headers credentials: true, // Allow credentials }, }; await fuse(app, options); ``` Here are the default CORS options: ```typescript export const defaultFastifyCorsOptions: FastifyCorsOptions = { origin: true, // Allow all origins methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'], // Allowed methods allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Bearer'], // Allowed headers exposedHeaders: ['Content-Length', 'X-Requested-With'], // Exposed headers credentials: true, // Allow credentials }; ``` # Open API and Docs UX `fastify-fusion` integrates with `fastify-swagger` and `scalar` to provide OpenAPI documentation and a user-friendly interface for exploring your API. By default, it serves the OpenAPI documentation at `/docs/json` and the Scalar UX at `/`. You can customize the route prefixes and the static path for the docs UX. You can customize the OpenAPI options by passing in an `OpenApiOptions` object to the `fuse` function. Here is an example of how to customize the OpenAPI options: ```typescript export type OpenApiOptions = { title?: string; description?: string; version?: string; openApiRoutePrefix?: string; docsRoutePath?: string; }; ``` ```typescript import { fuse, FuseOptions } from 'fastify-fusion'; import Fastify from 'fastify'; const app = Fastify(); const options: FuseOptions = { openApi: { openApiRoutePrefix: '/api-docs', // Change the OpenAPI JSON route prefix docsRoutePath: '/docs', // Change the OpenAPI docs route prefix }, }; await fuse(app, options); ``` You can also set it to `false` to disable the OpenAPI documentation and Scalar UX: ```typescript import { fuse, FuseOptions } from 'fastify-fusion'; import Fastify from 'fastify'; const app = Fastify(); const options: FuseOptions = { openApi: false, // Disable OpenAPI documentation and Scalar UX }; await fuse(app, options); ``` # How to Contribute If you want to contribute to this project, please read the [Contributing Guide](./CONTRIBUTING.md) for more information on how to get started. # Licensing and Copyright This project is licensed under the [MIT License](./LICENSE). Copyright (c) Jared Wray.