UNPKG

fastify-fusion

Version:

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

361 lines (347 loc) 10.8 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/index.ts import process from "process"; // src/cacheable.ts import { Cacheable } from "cacheable"; var defaultCacheableOptions = { ttl: "1h", stats: true, nonBlocking: true }; async function fuseCacheable(fastify, options = defaultCacheableOptions) { const cacheable = new Cacheable(options); fastify.decorate("cache", cacheable); fastify.addHook("onClose", async () => { await cacheable.disconnect(); }); fastify.log.info(`Fastify Cacheable Registered: ${JSON.stringify(options)}`); } __name(fuseCacheable, "fuseCacheable"); // src/fuse.ts import path2 from "path"; // src/cors.ts import { fastifyCors } from "@fastify/cors"; var defaultFastifyCorsOptions = { origin: true, methods: [ "GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS" ], allowedHeaders: [ "Content-Type", "Authorization", "X-Requested-With", "Bearer" ], exposedHeaders: [ "Content-Length", "X-Requested-With" ], credentials: true }; async function fuseCors(fastify, options) { await fastify.register(fastifyCors, options); fastify.log.info(`Fasity CORS Registered: ${JSON.stringify(options)}`); } __name(fuseCors, "fuseCors"); // src/helmet.ts import { fastifyHelmet } from "@fastify/helmet"; var defaultFastifyHelmetOptions = { // 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: 31536e3, 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 }; async function fuseHelmet(fastify, options) { await fastify.register(fastifyHelmet, options); fastify.log.info(`Fasity Helment Registered: ${JSON.stringify(options)}`); } __name(fuseHelmet, "fuseHelmet"); // src/log.ts import pino from "pino"; var defaultLoggingOptions = { transport: { target: "pino-pretty", options: { colorize: true, translateTime: true, ignore: "pid,hostname", singleLine: true } } }; async function fuseLog(fastify, options) { fastify.log = pino(options); fastify.log.info(`Fasity Logging Registered: ${JSON.stringify(options)}`); } __name(fuseLog, "fuseLog"); function logger(options) { const options_ = options ?? defaultLoggingOptions; return pino(options_); } __name(logger, "logger"); // src/open-api.ts import { fastifySwagger } from "@fastify/swagger"; import { fastifySwaggerUi } from "@fastify/swagger-ui"; import { readPackageUp } from "read-package-up"; var defaultOpenApiOptions = { title: "Open API Documentation", description: "API Documentation for the Service", version: "0.0.0", openApiRoutePrefix: "/openapi", docsRoutePath: "/" }; var fastifySwaggerConfig = { openapi: { info: { title: "Open API Documentation", description: "API Documentation for the Service", version: "0.0.0" }, consumes: [ "application/json" ], produces: [ "application/json" ] } }; async function fuseOpenApi(fastify, options) { const config = structuredClone(defaultOpenApiOptions); const pkg = await readPackageUp(); config.title = options?.title ?? pkg?.packageJson?.name ?? config.title; config.description = options?.description ?? pkg?.packageJson?.description ?? config.description; config.version = options?.version ?? pkg?.packageJson?.version ?? config.version; config.openApiRoutePrefix = options?.openApiRoutePrefix ?? config.openApiRoutePrefix; config.docsRoutePath = options?.docsRoutePath ?? config.docsRoutePath; const swaggerConfig = structuredClone(fastifySwaggerConfig); swaggerConfig.openapi.info.title = config.title; swaggerConfig.openapi.info.description = config.description; swaggerConfig.openapi.info.version = config.version; await fastify.register(fastifySwagger, swaggerConfig); await fastify.register(fastifySwaggerUi, { routePrefix: config.openApiRoutePrefix, uiConfig: { docExpansion: "none", deepLinking: false }, uiHooks: { /* c8 ignore next 6 */ onRequest(_request, _reply, next) { next(); }, preHandler(_request, _reply, next) { next(); } }, // eslint-disable-next-line @typescript-eslint/naming-convention staticCSP: true, transformSpecification: /* @__PURE__ */ __name((swaggerObject, _request, _reply) => swaggerObject, "transformSpecification"), transformSpecificationClone: true }); fastify.log.info(`Fastify OpenAPI Registered: ${JSON.stringify(config)}`); await indexRoute(fastify, config); fastify.log.info(`Fastify API Docs Registered: ${config.docsRoutePath}`); } __name(fuseOpenApi, "fuseOpenApi"); async function indexRoute(fastify, options) { const indexPath = options?.docsRoutePath ?? defaultOpenApiOptions.docsRoutePath; fastify.get(indexPath, { schema: { hide: true } }, async (_request, reply) => { const title = options?.title ?? defaultOpenApiOptions.title; const openApiRoutePrefix = options?.openApiRoutePrefix ?? defaultOpenApiOptions.openApiRoutePrefix; const redocHtml = ` <!doctype html> <html> <head> <title>${title}</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> </head> <body> <script id="api-reference" data-url="${openApiRoutePrefix}/json"></script> <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.31.17/dist/browser/standalone.min.js"></script> </body> </html> `; await reply.type("text/html; charset=utf-8").send(redocHtml); }); } __name(indexRoute, "indexRoute"); // src/rate-limit.ts import { fastifyRateLimit } from "@fastify/rate-limit"; var defaultFastifyRateLimitOptions = { // Enable rate limiting global: true, // Limit to 100 requests per minute max: 500, // Time window for the rate limit timeWindow: 6e4, // allow list for local development and testing allowList: [ "127.0.0.1", "0.0.0.0" ] }; async function fuseRateLimit(fastify, options) { await fastify.register(fastifyRateLimit, options); fastify.log.info(`Fasity Rate Limit Registered: ${JSON.stringify(options)}`); } __name(fuseRateLimit, "fuseRateLimit"); // src/static.ts import path from "path"; import fastifyStatic from "@fastify/static"; async function fuseStatic(fastify, options) { for (const staticPath of options) { let rootPath = staticPath.dir; if (!path.isAbsolute(rootPath)) { rootPath = path.resolve(rootPath); } await fastify.register(fastifyStatic, { root: rootPath, prefix: staticPath.path, decorateReply: false }); fastify.log.info(`Static path registered: ${staticPath.path} -> ${rootPath}`); } } __name(fuseStatic, "fuseStatic"); // src/fuse.ts async function fuse(fastify, options) { options ??= { static: true, log: true, helmet: true, rateLimit: true, cors: true, openApi: true, cache: true }; if (options.log !== void 0 && typeof options.log !== "boolean") { await fuseLog(fastify, options.log); } else if (options.log !== false) { await fuseLog(fastify, defaultLoggingOptions); } if (options.static !== void 0 && typeof options.static !== "boolean") { await fuseStatic(fastify, options.static); } else if (options.static !== false) { const defaultStaticPath = [ { dir: path2.resolve("./public"), path: "/" } ]; await fuseStatic(fastify, defaultStaticPath); } if (options.helmet !== void 0 && typeof options.helmet !== "boolean") { await fuseHelmet(fastify, options.helmet); } else if (options.helmet !== false) { await fuseHelmet(fastify, defaultFastifyHelmetOptions); } if (options.rateLimit !== void 0 && typeof options.rateLimit !== "boolean") { await fuseRateLimit(fastify, options.rateLimit); } else if (options.rateLimit !== false) { await fuseRateLimit(fastify, defaultFastifyRateLimitOptions); } if (options.cors !== void 0 && typeof options.cors !== "boolean") { await fuseCors(fastify, options.cors); } else if (options.cors !== false) { await fuseCors(fastify, defaultFastifyCorsOptions); } if (options.openApi !== void 0 && typeof options.openApi !== "boolean") { await fuseOpenApi(fastify, options.openApi); } else if (options.openApi !== false) { await fuseOpenApi(fastify); } if (options.cache !== void 0 && typeof options.cache !== "boolean") { await fuseCacheable(fastify, options.cache); } else if (options.cache !== false) { await fuseCacheable(fastify, defaultCacheableOptions); } } __name(fuse, "fuse"); // src/index.ts var defaultStartOptions = { port: 3e3, host: "0.0.0.0", message: /* @__PURE__ */ __name((host, port) => `\u{1F30F} started successfully at http://${host}:${port}`, "message") }; async function start(fastify, options = defaultStartOptions) { try { const portString = process.env.PORT ?? options.port; const host = process.env.HOST ?? options.host; if (portString === void 0 || portString === null || Number.isNaN(portString)) { throw new Error("Port is not defined. Please set the PORT environment variable or provide a port in the options."); } const port = Number(portString); if (host === void 0 || host === null || host.trim() === "") { throw new Error("Host is not defined. Please set the HOST environment variable or provide a host in the options."); } await fastify.listen({ port, host }); } catch (error) { fastify.log.error(error); } } __name(start, "start"); export { defaultCacheableOptions, defaultFastifyHelmetOptions, defaultFastifyRateLimitOptions, defaultLoggingOptions, defaultStartOptions, fuse, fuseCacheable, fuseHelmet, fuseLog, fuseOpenApi, fuseRateLimit, fuseStatic, logger, start };