@tsdiapi/server
Version:
A fully ESM-based, modular TypeScript server built on Fastify
970 lines (869 loc) • 34.9 kB
text/typescript
import { FastifyError, FastifyInstance, FastifyReply, FastifyRequest, RouteOptions } from 'fastify';
import { Static, TDate, TSchema, Type, } from '@sinclair/typebox';
import { RateLimitOptions } from '@fastify/rate-limit';
import { AppContext, UploadFile } from './types.js';
import { fileTypeFromBuffer } from 'file-type';
import { MetaSchemaStorage, MetaRouteEntry, metaRouteSchemaStorage } from './meta.js';
import { ResponseError } from './response.js';
export type FileOptions = {
maxFileSize?: number;
accept?: string[];
maxFiles?: number;
};
export function DateString(defaultValue?: string | Date) {
return Type.String({
format: 'date-time',
...(defaultValue ? {
default: new Date(defaultValue).toISOString()
} : {})
}) as unknown as TDate;
}
function groupFilesByFieldname(files: UploadFile[]): Record<string, UploadFile[]> {
return files.reduce<Record<string, UploadFile[]>>((acc, file) => {
if (!acc[file.fieldname]) {
acc[file.fieldname] = [];
}
acc[file.fieldname].push(file);
return acc;
}, {});
}
export type OnSendHook = (this: RouteBuilder, request: RequestWithState, reply: FastifyReply, payload: unknown) => void | Promise<void>;
export type PreValidationHook = (this: RouteBuilder, request: RequestWithState, reply: FastifyReply) => void | Promise<void> | false;
export type OnRequestHook = (this: RouteBuilder, request: RequestWithState, reply: FastifyReply) => void | Promise<void>;
export type PreSerializationHook = (
this: RouteBuilder,
request: RequestWithState,
reply: FastifyReply,
payload: unknown
) => void | Promise<void>;
export type OnResponseHook = (this: RouteBuilder, request: RequestWithState, reply: FastifyReply) => void | Promise<void>;
export type OnErrorHook = (this: RouteBuilder, error: FastifyError, request: RequestWithState, reply: FastifyReply) => void | Promise<void>;
export type ErrorHandlerHook = (
this: RouteBuilder,
error: FastifyError,
request: RequestWithState,
reply: FastifyReply
) => void | Promise<void>;
export type PreParsingHook = (
this: RouteBuilder,
request: RequestWithState,
reply: FastifyReply,
payload: unknown
) => void | Promise<void>;
export type GuardFn<TResponses extends Record<number, TSchema>, TState> = (
this: RouteBuilder,
request: RequestWithState,
reply: FastifyReply
) => boolean | ResponseUnion<TResponses> | Promise<boolean | ResponseUnion<TResponses>> | void | Promise<void>;
export type StatusSchemas = Record<number, TSchema>;
export type ResponseUnion<TResponses extends StatusSchemas> =
{
[K in keyof TResponses]:
K extends number
? { status: K; data: Static<TResponses[K]> }
: never;
}[keyof TResponses];
type MergeStatus<
TCurrent extends StatusSchemas,
Code extends number,
Schema extends TSchema
> = TCurrent & { [P in Code]: Schema };
export type HookType = 'preHandler' | 'onRequest' | 'preValidation' | 'preParsing' | 'preSerialization' | 'onSend' | 'onResponse' | 'onError';
export type PrehandlerFn = (this: RouteBuilder, req: RequestWithState, reply: FastifyReply) => Promise<unknown> | unknown;
export type HandlerFn = (this: RouteBuilder, req: RequestWithState, reply: FastifyReply) => Promise<unknown> | unknown;
export interface RouteConfig<TState = unknown> {
method: string;
url: string;
modify?: (routeConfig: RouteOptions) => Promise<RouteOptions> | RouteOptions;
schema: {
params?: TSchema;
body?: TSchema;
querystring?: TSchema;
headers?: TSchema;
response?: Record<number, TSchema>;
consumes?: string[];
};
errorHandler?: ErrorHandlerHook;
fileOptions?: Record<string, FileOptions>;
rateLimit?: false | RateLimitOptions;
guards: Array<GuardFn<StatusSchemas, TState>>;
preHandlers: Array<PrehandlerFn> | null;
preValidation: PreValidationHook | null;
preParsing: PreParsingHook | null;
preSerialization: PreSerializationHook | null;
onRequest: OnRequestHook | null;
onSend: OnSendHook | null;
onResponse: OnResponseHook | null;
onError: OnErrorHook | null;
resolver?: (
req: FastifyRequest,
reply: FastifyReply
) => Promise<TState | ResponseUnion<StatusSchemas>> | (TState | ResponseUnion<StatusSchemas>);
responseHeaders: Record<string, string>;
isMultipart?: boolean;
responseType?: string;
cacheControl?: string;
handler?: HandlerFn;
prefix?: string;
controller?: string;
tags?: string[];
summary?: string;
version?: string;
description?: string;
security?: Array<{ [key: string]: string[] }>;
operationId?: string;
}
export function trimSlashes(input: string): string {
return input.replace(/^[/\\]+|[/\\]+$/g, '');
}
declare module 'fastify' {
interface FastifyRequest {
routeData?: unknown;
tempFiles?: Array<UploadFile>;
session?: {
user?: Record<any, any>; // User data from session authentication
jwt?: Record<any, any>; // JWT token data
isAuthenticated?: boolean; // Authentication flag
destroy(callback?: (err?: Error) => void): void; // Method from @fastify/session
regenerate(): Promise<void>; // Method from @fastify/session
}
}
}
export type RequestWithState<
Params extends TSchema = TSchema,
Body extends TSchema = TSchema,
Query extends TSchema = TSchema,
Headers extends TSchema = TSchema,
TState = unknown
> = FastifyRequest<
{
Params: Static<Params>;
Body: Static<Body>;
Querystring: Static<Query>;
Headers: Static<Headers>;
}
> & {
routeData: TState;
};
export class RouteBuilder<
Params extends TSchema = TSchema,
Body extends TSchema = TSchema,
Query extends TSchema = TSchema,
Headers extends TSchema = TSchema,
TResponses extends StatusSchemas = {},
TState = unknown
> {
private extraMetaStorage: MetaSchemaStorage = new MetaSchemaStorage();
private config: RouteConfig<TState> = {
method: 'GET',
url: '',
schema: { response: {} },
guards: [],
preHandlers: [],
prefix: '/api',
preValidation: null,
version: null,
preParsing: null,
preSerialization: null,
onRequest: null,
onSend: null,
onResponse: null,
onError: null,
responseHeaders: {},
};
withRef<T extends TSchema>(schema: T): TSchema {
if (schema && schema.$id) {
if (!this.fastify.getSchema(schema.$id)) {
this.fastify.addSchema(schema);
}
return Type.Ref(schema.$id);
}
return schema;
}
fastify: FastifyInstance;
constructor(private appContext: AppContext<Record<string, any>>) {
this.fastify = appContext.fastify;
}
public setRequestFormat(contentType: string): this {
if (contentType === 'multipart/form-data') {
this.config.isMultipart = true;
}
this.config.schema.consumes = [contentType];
return this;
}
public controller(controllerPath: string): this {
const cleanedPath = trimSlashes(controllerPath);
const [controller, ...rest] = cleanedPath.split('/');
const remainingUrl = rest.join('/');
this.config.controller = controller;
if (remainingUrl) {
this.config.url = `/${remainingUrl}`;
}
this.tags([controller]);
return this;
}
public acceptJson(): this {
return this.setRequestFormat('application/json');
}
public acceptMultipart(): this {
return this.setRequestFormat('multipart/form-data');
}
public acceptText(): this {
return this.setRequestFormat('text/plain');
}
public setResponseFormat(contentType: string): this {
this.config.responseType = contentType;
return this;
}
public json(): this {
return this.setResponseFormat('application/json');
}
public binary(): this {
return this.setResponseFormat('application/octet-stream');
}
public rawResponse(contentType: string): this {
return this.setResponseFormat(contentType);
}
public multipart(): this {
return this.setResponseFormat('multipart/form-data');
}
public consumes(consumes: string[]): this {
this.config.schema.consumes = consumes;
return this;
}
public text(): this {
return this.setResponseFormat('text/plain');
}
public responseType(type: string): this {
this.config.responseType = type;
return this;
}
public version(version: string): this {
this.config.version = version;
return this;
}
// -------------------------
// 2) HTTP methods
// -------------------------
public get(path?: string): this {
this.config.method = 'GET';
if (path) {
this.config.url = path;
}
return this;
}
public post(path?: string): this {
this.config.method = 'POST';
if (path) {
this.config.url = path;
}
return this;
}
public put(path?: string): this {
this.config.method = 'PUT';
if (path) {
this.config.url = path;
}
return this;
}
public delete(path?: string): this {
this.config.method = 'DELETE';
if (path) {
this.config.url = path;
}
return this;
}
public patch(path?: string): this {
this.config.method = 'PATCH';
if (path) {
this.config.url = path;
}
return this;
}
public options(path?: string): this {
this.config.method = 'OPTIONS';
if (path) {
this.config.url = path;
}
return this;
}
// Swagger compatibility
public tags(tags: string[]): this {
this.config.tags = tags;
return this;
}
public summary(summary: string): this {
this.config.summary = summary;
return this;
}
public description(description: string): this {
this.config.description = description;
return this;
}
public operationId(id: string): this {
this.config.operationId = id;
return this;
}
private generateOperationId(): string {
const { method, url, controller } = this.config;
const parts: string[] = [];
// Add controller if exists
if (controller) {
parts.push(controller.toLowerCase());
}
// Add HTTP method
parts.push(method.toLowerCase());
// Process URL path
const urlParts = url.split('/').filter(Boolean);
parts.push(...urlParts.map(part => part.toLowerCase()));
// Join all parts with underscore
return parts.join('_');
}
public auth(type: "bearer" | "basic" | "apiKey" = "bearer", guard?: GuardFn<TResponses, TState>): this {
if (!this.config.schema.headers) {
this.config.schema.headers = Type.Object({});
}
if (guard) {
this.guard(guard);
}
const securityName = type === "bearer"
? "BearerAuth"
: type === "basic"
? "BasicAuth"
: "ApiKeyAuth";
if (!this.config.security) {
this.config.security = [];
}
if (!this.config.security.some(s => securityName in s)) {
this.config.security.push({ [securityName]: [] });
}
return this;
}
// --------------------------
// 3) Schema definition
// --------------------------
public params<T extends TSchema>(
schema: T
): RouteBuilder<T, Body, Query, Headers, TResponses, TState> {
this.extraMetaStorage.add({
type: 'params',
schema: schema,
id: schema.$id || undefined
});
this.config.schema.params = this.withRef(schema);
return this as unknown as RouteBuilder<T, Body, Query, Headers, TResponses, TState>;
}
public body<T extends TSchema>(
schema: T
): RouteBuilder<Params, T, Query, Headers, TResponses, TState> {
this.extraMetaStorage.add({
type: 'body',
schema: schema,
id: schema.$id || undefined
});
this.config.schema.body = this.withRef(schema);
return this as unknown as RouteBuilder<Params, T, Query, Headers, TResponses, TState>;
}
public query<T extends TSchema>(
schema: T
): RouteBuilder<Params, Body, T, Headers, TResponses, TState> {
this.extraMetaStorage.add({
type: 'query',
schema: schema,
id: schema.$id || undefined
});
this.config.schema.querystring = this.withRef(schema);
return this as unknown as RouteBuilder<Params, Body, T, Headers, TResponses, TState>;
}
public headers<T extends TSchema>(
schema: T
): RouteBuilder<Params, Body, Query, T, TResponses, TState> {
this.extraMetaStorage.add({
type: 'headers',
schema: schema,
id: schema.$id || undefined
});
this.config.schema.headers = this.withRef(schema);
return this as unknown as RouteBuilder<Params, Body, Query, T, TResponses, TState>;
}
public code<
Code extends number,
T extends TSchema
>(
code: Code,
schema: T
): RouteBuilder<Params, Body, Query, Headers, MergeStatus<TResponses, Code, T>, TState> {
if (code === 204) {
return this.codes({ [code]: Type.Object({}) });
}
return this.codes({ [code]: schema });
}
public codes<
TNewResponses extends Record<number, TSchema>
>(
responses: TNewResponses
): RouteBuilder<Params, Body, Query, Headers, TResponses & TNewResponses, TState> {
for (const [code, schema] of Object.entries(responses)) {
const statusCode = Number(code);
this.extraMetaStorage.add({
type: 'response',
statusCode,
schema,
id: schema.$id || undefined
});
this.config.schema.response[statusCode] = Type.Object({
status: Type.Literal(statusCode),
data: this.withRef(schema)
});
}
return this as unknown as RouteBuilder<Params, Body, Query, Headers, TResponses & TNewResponses, TState>;
}
// --------------------------
// 4) Guard functions
// --------------------------
public guard(
fn: (
this: RouteBuilder,
request: RequestWithState<Params, Body, Query, Headers, TState>,
reply: FastifyReply
) => boolean | ResponseUnion<TResponses> | Promise<boolean | ResponseUnion<TResponses>> | void | Promise<void>
): this {
this.config.guards.push(async (req, reply) => {
try {
const result = await fn.call(this, req, reply);
if (result === true || result === undefined) return true;
if ((typeof result === "object") && ("status" in result) && ("data" in result)) {
reply.code(result.status).send(result);
return false;
}
return reply.code(500).send(result?.message || `Guard returned an invalid error object`);
} catch (error) {
if (error instanceof ResponseError) {
reply.code(error.status).send(error);
return false;
}
if ("status" in error && "data" in error) {
reply.code(error.status).send(error);
return false;
}
return reply.code(500).send(error?.message || `Unknown server error`);
}
});
return this;
}
// --------------------------
// 5) Hooks
// --------------------------
public onRequest(fn: (this: RouteBuilder, request: RequestWithState<Params, Body, Query, Headers, TState>, reply: FastifyReply) => void | Promise<void>): this {
this.config.onRequest = fn;
return this;
}
public preValidation(fn: (this: RouteBuilder, request: RequestWithState<Params, Body, Query, Headers, TState>, reply: FastifyReply) => void | Promise<void> | false): this {
this.config.preValidation = fn;
return this;
}
public preParsing(fn: (this: RouteBuilder, request: RequestWithState<Params, Body, Query, Headers, TState>, reply: FastifyReply, payload: unknown) => void | Promise<void>): this {
this.config.preParsing = fn;
return this;
}
public preSerialization(fn: (this: RouteBuilder, request: RequestWithState<Params, Body, Query, Headers, TState>, reply: FastifyReply, payload: unknown) => void | Promise<void>): this {
this.config.preSerialization = fn;
return this;
}
public preHandler(fn: (this: RouteBuilder, request: RequestWithState<Params, Body, Query, Headers, TState>, reply: FastifyReply) => void | Promise<void>): this {
this.config.preHandlers.push(fn);
return this;
}
public onSend(fn: (this: RouteBuilder, request: RequestWithState<Params, Body, Query, Headers, TState>, reply: FastifyReply, payload: unknown) => void | Promise<void>): this {
this.config.onSend = fn;
return this;
}
public onResponse(fn: (this: RouteBuilder, request: RequestWithState<Params, Body, Query, Headers, TState>, reply: FastifyReply) => void | Promise<void>): this {
this.config.onResponse = fn;
return this;
}
public onError(fn: (this: RouteBuilder, error: FastifyError, request: RequestWithState<Params, Body, Query, Headers, TState>, reply: FastifyReply) => void | Promise<void>): this {
this.config.onError = fn;
return this;
}
public setErrorHandler(fn: (this: RouteBuilder, error: FastifyError, request: RequestWithState<Params, Body, Query, Headers, TState>, reply: FastifyReply) => void | Promise<void>): this {
this.config.errorHandler = fn;
return this;
}
// -------------------------------------------
// 6) Data passing between hooks (resolver)
// -------------------------------------------
public resolve<TNewState extends TState>(
fn: (
req: RequestWithState<Params, Body, Query, Headers, TState>,
reply: FastifyReply
) => TNewState | Promise<TNewState>
): RouteBuilder<Params, Body, Query, Headers, TResponses, TNewState> {
this.config.resolver = fn;
return this as unknown as RouteBuilder<
Params,
Body,
Query,
Headers,
TResponses,
TNewState
>;
}
// --------------------------------
// 7) Final handler
// --------------------------------
public handler(
fn: (
req: RequestWithState<Params, Body, Query, Headers, TState>,
reply: FastifyReply
) => Promise<ResponseUnion<TResponses> | string> | (ResponseUnion<TResponses> | string)
): this {
this.config.handler = fn;
return this;
}
// ------------------------------------------------
// 8) Custom response headers and Cache-Control
// ------------------------------------------------
public responseHeader<Code extends keyof TResponses>(
name: string,
value: string,
statusCode: Code // ✅ Limit only to registered statuses
): this {
if (!(statusCode in this.config.schema.response)) {
throw new Error(
`Cannot add header to status ${statusCode.toString()}: this status is not defined in .success() or .error()`
);
}
this.config.responseHeaders[name] = value;
const schema = this.config.schema.response[statusCode as number];
if (schema && typeof schema === 'object') {
if (!schema.properties) {
schema.properties = {};
}
schema.properties[name] = Type.String(); // Swagger requires type
}
return this;
}
public cacheControl(value: string): this {
this.config.cacheControl = value;
return this;
}
public rateLimit(options: false | RateLimitOptions): this {
this.config.rateLimit = options;
return this;
}
public fileOptions(
options: FileOptions,
key?: keyof Static<Body>
): this {
if (!this.config.fileOptions) {
this.config.fileOptions = {};
}
const field = key || 'default';
this.config.fileOptions[field as string] = options;
return this;
}
public modify(
fn: (routeConfig: RouteOptions & { state?: TState }) => Promise<RouteOptions> | RouteOptions
): this {
this.config.modify = fn;
return this;
}
public async build(): Promise<void> {
const {
method,
url,
schema,
guards,
resolver,
handler,
responseHeaders,
responseType,
cacheControl,
rateLimit,
modify,
tags,
description,
summary,
security,
isMultipart,
fileOptions,
errorHandler,
preHandlers,
preParsing,
preValidation,
preSerialization,
onRequest,
onSend,
onResponse,
onError,
version,
prefix,
controller,
operationId
} = this.config;
if (!handler) {
throw new Error('Handler is required');
}
if (!method || !url) {
throw new Error('Method and URL are required');
}
const resolvePreHandler = async (req: FastifyRequest, reply: FastifyReply) => {
if (resolver) {
try {
const result = await resolver(req, reply);
if (result instanceof ResponseError) {
reply.code(result.status).send(result);
return false;
}
if ((typeof result === "object") && ("status" in result) && ("data" in result)) {
reply.code(result.status).send({
status: result.status,
data: result.data
});
return false;
}
req.routeData = result as TState;
} catch (error) {
if (error instanceof ResponseError) {
reply.code(error.status).send(error);
return false;
}
return reply.code(500).send({
status: 500,
data: { error: error.message },
});
}
}
return true;
};
const preHandlersWithResolver = [resolvePreHandler, ...guards];
const tempFilesPrehandler = async (req: FastifyRequest) => {
if (Array.isArray(req.tempFiles) && req.tempFiles.length) {
const files = groupFilesByFieldname(req.tempFiles);
if (!req.body) {
req.body = {};
}
for (const [key, value] of Object.entries(files)) {
const urls = value.filter(f => f.url).map(file => file.url);
const isArray = (req.body as any)[key] instanceof Array;
if (isArray) {
(req.body as any)[key] = urls.length ? urls : [];
} else {
(req.body as any)[key] = urls[0] || null;
}
}
}
}
const allPreHandlers = [...preHandlersWithResolver, ...preHandlers];
if (isMultipart && fileOptions) {
allPreHandlers.push(tempFilesPrehandler);
}
const extendedSchema: Record<string, any> = {
tags: tags || [],
summary: summary || '',
description: description || '',
security: security || [],
operationId: operationId || this.generateOperationId()
}
if (schema.body) {
extendedSchema.body = schema.body;
}
if (schema.querystring) {
extendedSchema.querystring = schema.querystring;
}
if (schema.headers) {
extendedSchema.headers = schema.headers;
}
if (schema.params) {
extendedSchema.params = schema.params;
}
if (schema.response) {
extendedSchema.response = schema.response;
}
if (schema.consumes) {
extendedSchema.consumes = schema.consumes;
}
const onErrorHandler = (error: FastifyError, req: FastifyRequest, reply: FastifyReply) => {
if (error) {
if (errorHandler) {
errorHandler.call(this, error, req, reply);
} else {
return reply.code(500).send({
status: 500,
data: { error: error.message },
});
}
}
}
const cleanedPrefix = trimSlashes(prefix || '');
const cleanedController = trimSlashes(controller || '');
const cleanedUrl = trimSlashes(url || '');
const _prefix = cleanedPrefix ? `${cleanedPrefix}/` : '';
const _controller = cleanedController ? `${cleanedController}/` : '';
const _version = version ? `v${version}/` : '';
const route = `/${_prefix}${_controller}${_version}${cleanedUrl}`;
const schemas = this.extraMetaStorage.getAll();
const metaEntry: MetaRouteEntry = {
route,
method,
meta: schemas
}
metaRouteSchemaStorage.add(metaEntry);
let newRouteOptions: RouteOptions = {
method,
url: route,
schema: extendedSchema,
config: rateLimit ? { rateLimit } : undefined,
preHandler: allPreHandlers.length ? allPreHandlers.map((fn) => async (req, reply) => {
const result = await fn.call(this, req, reply);
if (result === false) {
return;
}
}) : undefined,
preValidation: async (req, reply) => {
if (preValidation) {
const result = await preValidation.call(this, req, reply);
if (result === false) {
return;
}
}
if (isMultipart && fileOptions) {
const tempFiles = Array.isArray(req.tempFiles) ? req.tempFiles : [];
const errors: string[] = [];
const fileCounts: Record<string, number> = {};
const defaultOptions = fileOptions.default || null;
for (const file of tempFiles) {
const options = fileOptions[file.fieldname] || defaultOptions;
if (!options) continue;
// Check maximum file size
if (options.maxFileSize && file.filesize > options.maxFileSize) {
errors.push(`File "${file.filename}" exceeds max size of ${options.maxFileSize} bytes.`);
}
if (options.accept) {
const fileType = await fileTypeFromBuffer(file.buffer);
const actualMime = fileType?.mime || file.mimetype;
const allowedTypes = options.accept.map(type =>
type.endsWith('/*') ? type.replace('/*', '') : type
);
const isValidMime = allowedTypes.some(type =>
actualMime.startsWith(type)
);
if (!isValidMime) {
errors.push(`File "${file.filename}" has an invalid MIME type "${actualMime}". Allowed: ${options.accept.join(', ')}`);
}
}
fileCounts[file.fieldname] = (fileCounts[file.fieldname] || 0) + 1;
}
for (const file of tempFiles) {
const options = fileOptions[file.fieldname] || defaultOptions;
if (options.maxFiles && (fileCounts[file.fieldname] || 0) > options.maxFiles) {
const messageError = `Field "${file.fieldname}" exceeds max allowed files (${options.maxFiles}).`;
if (!errors.includes(messageError)) {
errors.push(`Field "${file.fieldname}" exceeds max allowed files (${options.maxFiles}).`);
}
}
}
if (errors.length > 0) {
reply.code(400).send({
status: 400,
data: {
error:
errors.join('\n')
},
});
} else {
if (this.appContext.fileLoader && Array.isArray(req.tempFiles)) {
for (const file of req.tempFiles) {
const uploadedFile = await this.appContext.fileLoader(file, this as unknown as RouteBuilder<TSchema, TSchema, TSchema, TSchema, {}, unknown>);
req.tempFiles[req.tempFiles.indexOf(file)] = uploadedFile;
}
}
}
}
},
errorHandler: onErrorHandler,
preSerialization: async (req, reply, payload) => {
if (preSerialization) {
await preSerialization.call(this, req, reply, payload);
}
},
preParsing: async (req, reply, payload) => {
if (preParsing) {
await preParsing.call(this, req, reply, payload);
}
},
onResponse: async (req, reply) => {
if (onResponse) {
await onResponse.call(this, req, reply);
}
},
onRequest: async (req, reply) => {
if (onRequest) {
await onRequest.call(this, req, reply);
}
},
onSend: async (req, reply, payload) => {
if (onSend) {
await onSend.call(this, req, reply, payload);
}
},
onError: async (error, req, reply) => {
if (onError) {
await onError.call(this, error, req, reply);
}
},
handler: async (req, reply) => {
for (const [headerName, headerValue] of Object.entries(responseHeaders)) {
reply.header(headerName, headerValue);
}
if (cacheControl) {
reply.header('Cache-Control', cacheControl);
}
if (responseType) {
reply.type(responseType);
}
if (handler) {
try {
const result = await handler.call(this, req, reply) as ResponseUnion<TResponses>;
if (result instanceof ResponseError) {
return reply.code(result.status).send(result);
}
if (
result &&
typeof result === 'object' &&
'status' in result
) {
return reply.code(result.status).send(result);
}
reply.type(this.config.responseType || 'text/html');
return result;
} catch (error) {
if (error instanceof ResponseError) {
return reply.code(error.status).send(error);
}
if ("status" in error && "data" in error) {
return reply.code(error.status).send({
status: error.status,
data: error.data
});
}
return reply.code(500).send({
status: 500,
data: { error: error.message || 'Internal server error' }
});
}
} else {
return reply.code(500).send({
status: 500,
data: { error: 'No handler provided' }
});
}
}
};
if (modify) {
newRouteOptions = await modify(newRouteOptions);
}
this.fastify.route(newRouteOptions);
}
}