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