Repository: RocketChat/Rocket.Chat
Pull request #34983: feat: add OpenAPI support
Author: login: ggazzo
Created: 2025-01-21T03:14:46Z | Merged: 2025-03-14T13:06:53Z

<!-- This is a pull request template, you do not need to uncomment or remove the comments, they won't show up in the PR text. -->
it can be accessed by `docs/json`
UI can be accessed by `/api-docs`
[ARCH-1550](https://rocketchat.atlassian.net/browse/ARCH-1550)
result :  https://rocketchat.apidocumentation.com/reference#tag/missing-documentation/POST/api/v1/rooms.cleanHistory
<!-- Your Pull Request name should start with one of the following tags
  feat: Adding a new feature
  refactor: A code change that doesn't change behavior (it doesn't add anything and doesn't fix anything)
  fix: For bug fixes that affect the end-user
  chore: For small tasks
  docs: For documentation
  ci: For updating CI configuration
  test: For adding tests
  i18n: For updating any translations
  regression: Issues created/reported/fixed during the development phase. kind of problem that never existed in production and that we don't need to list in a changelog for the end user
-->

<!-- Checklist!!! If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code. 
  - I have read the Contributing Guide - https://github.com/RocketChat/Rocket.Chat/blob/develop/.github/CONTRIBUTING.md#contributing-to-rocketchat doc
  - I have signed the CLA - https://cla-assistant.io/RocketChat/Rocket.Chat
  - Lint and unit tests pass locally with my changes
  - I have added tests that prove my fix is effective or that my feature works (if applicable)
  - I have added necessary documentation (if applicable)
  - Any dependent changes have been merged and published in downstream modules
-->

## Proposed changes (including videos or screenshots)
<!--
  Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
  If it fixes a bug or resolves a feature request, be sure to link to that issue below.
  This description won't be displayed to our end users in the release notes, so feel free to add as much technical context as needed.
  If the changes introduced in this pull request must be presented in the release notes, make sure to add a changeset file. Check our guidelines for adding a changeset to your pull request: https://developer.rocket.chat/contribute-to-rocket.chat/modes-of-contribution/participate-in-rocket.chat-development/development-workflow#4.-adding-changeset-to-your-pull-request 
-->

## Issue(s)
<!-- Link the issues being closed by or related to this PR. For example, you can use #594 if this PR closes issue number 594 -->

## Steps to test or reproduce
<!-- Mention how you would reproduce the bug if not mentioned on the issue page already. Also mention which screens are going to have the changes if applicable -->

## Further comments
<!-- If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->


---

This pull request introduces a proof of concept for OpenAPI integration into the Rocket.Chat API. It focuses on enhancing type safety, documentation, and validation across various API components. Key changes include:

- Addition of typed route handling capabilities in `api.ts`, allowing for OpenAPI-like documentation and validation.
- Extension of API type definitions in `definition.ts` with new TypeScript types for improved type safety and error handling.
- Introduction of an `openApi` import in `index.ts`, with noted potential for better code organization and documentation.
- Minor improvements in type safety, error handling, and documentation in `openApi.ts`.
- Refactoring of the `custom-sounds.ts` API endpoint to include comprehensive response validation using AJV and improved type definitions.
- Transition of `federation.ts` API endpoints to use chained get methods, with added AJV schema validation for responses and enhanced type safety.
- Restructuring of `invites.ts` API route definitions to use chained methods and inclusion of response validation schemas using AJV for better organization.

[ARCH-1388]: https://rocketchat.atlassian.net/browse/ARCH-1388?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

[ARCH-1550]: https://rocketchat.atlassian.net/browse/ARCH-1550?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/.changeset/late-buttons-cover.md b/.changeset/late-buttons-cover.md
new file mode 100644
index 0000000000000..c047847e7e4c5
--- /dev/null
+++ b/.changeset/late-buttons-cover.md
@@ -0,0 +1,6 @@
+---
+"@rocket.chat/meteor": minor
+"@rocket.chat/rest-typings": minor
+---
+
+feat: Adds OpenAPI support
diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts
index cd274fe8f72cb..1b2741d8d7384 100644
--- a/apps/meteor/app/api/server/api.ts
+++ b/apps/meteor/app/api/server/api.ts
@@ -3,7 +3,9 @@ import { Logger } from '@rocket.chat/logger';
 import { Users } from '@rocket.chat/models';
 import { Random } from '@rocket.chat/random';
 import type { JoinPathPattern, Method } from '@rocket.chat/rest-typings';
+import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv';
 import { wrapExceptions } from '@rocket.chat/tools';
+import type { ValidateFunction } from 'ajv';
 import express from 'express';
 import type { Request, Response } from 'express';
 import { Accounts } from 'meteor/accounts-base';
@@ -29,6 +31,8 @@ import type {
 	PartialThis,
 	SuccessResult,
 	TypedThis,
+	TypedAction,
+	TypedOptions,
 	UnauthorizedResult,
 } from './definition';
 import { getUserInfo } from './helpers/getUserInfo';
@@ -37,6 +41,7 @@ import { cors } from './middlewares/cors';
 import { loggerMiddleware } from './middlewares/logger';
 import { metricsMiddleware } from './middlewares/metrics';
 import { tracerSpanMiddleware } from './middlewares/tracer';
+import type { Route } from './router';
 import { Router } from './router';
 import { isObject } from '../../../lib/utils/isObject';
 import { getNestedProp } from '../../../server/lib/getNestedProp';
@@ -141,7 +146,14 @@ const generateConnection = (
 	clientAddress: ipAddress,
 });
 
-export class APIClass<TBasePath extends string = ''> {
+export class APIClass<
+	TBasePath extends string = '',
+	TOperations extends {
+		[x: string]: unknown;
+	} = {},
+> {
+	public typedRoutes: Record<string, Record<string, Route>> = {};
+
 	protected apiPath?: string;
 
 	readonly version?: string;
@@ -496,6 +508,182 @@ export class APIClass<TBasePath extends string = ''> {
 		return routeActions.map((action) => this.getFullRouteName(route, action));
 	}
 
+	private registerTypedRoutesLegacy<TSubPathPattern extends string, TOptions extends Options>(
+		method: Method,
+		subpath: TSubPathPattern,
+		options: TOptions,
+	): void {
+		const { authRequired, validateParams } = options;
+
+		const opt = {
+			authRequired,
+			...(validateParams &&
+				method.toLowerCase() === 'get' &&
+				('GET' in validateParams
+					? { query: validateParams.GET }
+					: {
+							query: validateParams as ValidateFunction<any>,
+						})),
+
+			...(validateParams &&
+				method.toLowerCase() === 'post' &&
+				('POST' in validateParams ? { query: validateParams.POST } : { body: validateParams as ValidateFunction<any> })),
+
+			...(validateParams &&
+				method.toLowerCase() === 'put' &&
+				('PUT' in validateParams ? { query: validateParams.PUT } : { body: validateParams as ValidateFunction<any> })),
+			...(validateParams &&
+				method.toLowerCase() === 'delete' &&
+				('DELETE' in validateParams ? { query: validateParams.DELETE } : { body: validateParams as ValidateFunction<any> })),
+
+			tags: ['Missing Documentation'],
+			response: {
+				200: ajv.compile({
+					type: 'object',
+					properties: {
+						success: { type: 'boolean' },
+						error: { type: 'string' },
+					},
+					required: ['success'],
+				}),
+			},
+		};
+
+		this.registerTypedRoutes(method, subpath, opt);
+	}
+
+	private registerTypedRoutes<
+		TSubPathPattern extends string,
+		TOptions extends TypedOptions,
+		TPathPattern extends `${TBasePath}/${TSubPathPattern}`,
+	>(method: Method, subpath: TSubPathPattern, options: TOptions): void {
+		const path = `/${this.apiPath}/${subpath}`.replaceAll('//', '/') as TPathPattern;
+		this.typedRoutes = this.typedRoutes || {};
+		this.typedRoutes[path] = this.typedRoutes[subpath] || {};
+		const { query, authRequired, response, body, tags, ...rest } = options;
+		this.typedRoutes[path][method.toLowerCase()] = {
+			...(response && {
+				responses: Object.fromEntries(
+					Object.entries(response).map(([status, schema]) => [
+						status,
+						{
+							description: '',
+							content: {
+								'application/json': 'schema' in schema ? { schema: schema.schema } : schema,
+							},
+						},
+					]),
+				),
+			}),
+			...(query && {
+				parameters: [
+					{
+						schema: query.schema,
+						in: 'query',
+						name: 'query',
+						required: true,
+					},
+				],
+			}),
+			...(body && {
+				requestBody: {
+					required: true,
+					content: {
+						'application/json': { schema: body.schema },
+					},
+				},
+			}),
+			...(authRequired && {
+				...rest,
+				security: [
+					{
+						userId: [],
+						authToken: [],
+					},
+				],
+			}),
+			tags,
+		};
+	}
+
+	private method<TSubPathPattern extends string, TOptions extends TypedOptions, TPathPattern extends `${TBasePath}/${TSubPathPattern}`>(
+		method: Method,
+		subpath: TSubPathPattern,
+		options: TOptions,
+		action: TypedAction<TOptions>,
+	): APIClass<
+		TBasePath,
+		| TOperations
+		| ({
+				method: Method;
+				path: TPathPattern;
+		  } & Omit<TOptions, 'response'>)
+	> {
+		this.addRoute([subpath], { ...options, typed: true }, { [method.toLowerCase()]: { action } } as any);
+		this.registerTypedRoutes(method, subpath, options);
+		return this;
+	}
+
+	get<TSubPathPattern extends string, TOptions extends TypedOptions, TPathPattern extends `${TBasePath}/${TSubPathPattern}`>(
+		subpath: TSubPathPattern,
+		options: TOptions,
+		action: TypedAction<TOptions>,
+	): APIClass<
+		TBasePath,
+		| TOperations
+		| ({
+				method: 'GET';
+				path: TPathPattern;
+		  } & Omit<TOptions, 'response'>)
+	> {
+		return this.method('GET', subpath, options, action);
+	}
+
+	post<TSubPathPattern extends string, TOptions extends TypedOptions, TPathPattern extends `${TBasePath}/${TSubPathPattern}`>(
+		subpath: TSubPathPattern,
+		options: TOptions,
+		action: TypedAction<TOptions>,
+	): APIClass<
+		TBasePath,
+		| TOperations
+		| ({
+				method: 'POST';
+				path: TPathPattern;
+		  } & Omit<TOptions, 'response'>)
+	> {
+		return this.method('POST', subpath, options, action);
+	}
+
+	put<TSubPathPattern extends string, TOptions extends TypedOptions, TPathPattern extends `${TBasePath}/${TSubPathPattern}`>(
+		subpath: TSubPathPattern,
+		options: TOptions,
+		action: TypedAction<TOptions>,
+	): APIClass<
+		TBasePath,
+		| TOperations
+		| ({
+				method: 'PUT';
+				path: TPathPattern;
+		  } & Omit<TOptions, 'response'>)
+	> {
+		return this.method('PUT', subpath, options, action);
+	}
+
+	delete<TSubPathPattern extends string, TOptions extends TypedOptions, TPathPattern extends `${TBasePath}/${TSubPathPattern}`>(
+		subpath: TSubPathPattern,
+		options: TOptions,
+		action: TypedAction<TOptions>,
+	): APIClass<
+		TBasePath,
+		| TOperations
+		| ({
+				method: 'DELETE';
+				path: TPathPattern;
+		  } & Omit<TOptions, 'response'>)
+	> {
+		return this.method('DELETE', subpath, options, action);
+	}
+
 	addRoute<TSubPathPattern extends string>(
 		subpath: TSubPathPattern,
 		operations: Operations<JoinPathPattern<TBasePath, TSubPathPattern>>,
@@ -699,7 +887,7 @@ export class APIClass<TBasePath extends string = ''> {
 				(operations[method as keyof Operations<TPathPattern, TOptions>] as Record<string, any>).logger = logger;
 				this.router[method.toLowerCase() as 'get' | 'post' | 'put' | 'delete'](
 					`/${route}`.replaceAll('//', '/'),
-					{} as any,
+					_options as TypedOptions,
 					(operations[method as keyof Operations<TPathPattern, TOptions>] as Record<string, any>).action as any,
 				);
 				this._routes.push({
@@ -707,6 +895,11 @@ export class APIClass<TBasePath extends string = ''> {
 					options: _options,
 					endpoints: operations[method as keyof Operations<TPathPattern, TOptions>] as unknown as Record<string, string>,
 				});
+
+				this.registerTypedRoutesLegacy(method as Method, route, {
+					...options,
+					...operations[method as keyof Operations<TPathPattern, TOptions>],
+				});
 			});
 		});
 	}
@@ -938,17 +1131,13 @@ export class APIClass<TBasePath extends string = ''> {
 	}
 }
 
-const createApi = function _createApi(options: { version?: string; apiPath?: string } = {}): APIClass {
-	return new APIClass(
-		Object.assign(
-			{
-				apiPath: 'api/',
-				useDefaultAuth: true,
-				prettyJson: process.env.NODE_ENV === 'development',
-			},
-			options,
-		) as IAPIProperties,
-	);
+const createApi = function _createApi(options: { version?: string; useDefaultAuth?: true } = {}): APIClass {
+	return new APIClass({
+		apiPath: '',
+		useDefaultAuth: false,
+		prettyJson: process.env.NODE_ENV === 'development',
+		...options,
+	});
 };
 
 export const API: {
@@ -982,12 +1171,10 @@ export const API: {
 	ApiClass: APIClass,
 	api: new Router('/api'),
 	v1: createApi({
-		apiPath: '',
 		version: 'v1',
+		useDefaultAuth: true,
 	}),
-	default: createApi({
-		apiPath: '',
-	}),
+	default: createApi({}),
 };
 
 settings.watch<string>('Accounts_CustomFields', (value) => {
diff --git a/apps/meteor/app/api/server/default/openApi.ts b/apps/meteor/app/api/server/default/openApi.ts
new file mode 100644
index 0000000000000..96570e7495720
--- /dev/null
+++ b/apps/meteor/app/api/server/default/openApi.ts
@@ -0,0 +1,90 @@
+import { isOpenAPIJSONEndpoint } from '@rocket.chat/rest-typings';
+import express from 'express';
+import { WebApp } from 'meteor/webapp';
+import swaggerUi from 'swagger-ui-express';
+
+import { settings } from '../../../settings/server';
+import { Info } from '../../../utils/rocketchat.info';
+import { API } from '../api';
+import type { Route } from '../router';
+
+const app = express();
+
+const getTypedRoutes = (
+	typedRoutes: Record<string, Record<string, Route>>,
+	{ withUndocumented = false }: { withUndocumented?: boolean } = {},
+): Record<string, Record<string, Route>> => {
+	if (withUndocumented) {
+		return typedRoutes;
+	}
+
+	return Object.entries(typedRoutes).reduce(
+		(acc, [path, methods]) => {
+			const filteredMethods = Object.entries(methods)
+				.filter(([_, options]) => !options?.tags?.includes('Missing Documentation'))
+				.reduce(
+					(acc, [method, options]) => {
+						acc[method] = options;
+						return acc;
+					},
+					{} as Record<string, Route>,
+				);
+
+			if (Object.keys(filteredMethods).length > 0) {
+				acc[path] = filteredMethods;
+			}
+
+			return acc;
+		},
+		{} as Record<string, Record<string, Route>>,
+	);
+};
+
+const makeOpenAPIResponse = (paths: Record<string, Record<string, Route>>) => ({
+	openapi: '3.0.3',
+	info: {
+		title: 'Rocket.Chat API',
+		description: 'Rocket.Chat API',
+		version: Info.version,
+	},
+	servers: [
+		{
+			url: settings.get('Site_Url'),
+		},
+	],
+	components: {
+		securitySchemes: {
+			userId: {
+				type: 'apiKey',
+				in: 'header',
+				name: 'X-User-Id',
+			},
+			authToken: {
+				type: 'apiKey',
+				in: 'header',
+				name: 'X-Auth-Token',
+			},
+		},
+		schemas: {},
+	},
+	paths,
+});
+
+API.default.addRoute(
+	'docs/json',
+	{ authRequired: false, validateParams: isOpenAPIJSONEndpoint },
+	{
+		get() {
+			const { withUndocumented = false } = this.queryParams;
+
+			return API.default.success(makeOpenAPIResponse(getTypedRoutes(API.v1.typedRoutes, { withUndocumented })));
+		},
+	},
+);
+
+app.use(
+	'/api-docs',
+	swaggerUi.serve,
+	swaggerUi.setup(makeOpenAPIResponse(getTypedRoutes(API.v1.typedRoutes, { withUndocumented: false }))),
+);
+WebApp.connectHandlers.use(app);
diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts
index ea50a2f584f0b..26bcd9ea70c98 100644
--- a/apps/meteor/app/api/server/definition.ts
+++ b/apps/meteor/app/api/server/definition.ts
@@ -238,6 +238,7 @@ export type TypedOptions = {
 	query?: ValidateFunction;
 	body?: ValidateFunction;
 	tags?: string[];
+	typed?: boolean;
 } & Options;
 
 export type TypedThis<TOptions extends TypedOptions, TPath extends string = ''> = {
diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts
index 00f1a62f1cbd9..1d036306679e2 100644
--- a/apps/meteor/app/api/server/index.ts
+++ b/apps/meteor/app/api/server/index.ts
@@ -48,5 +48,6 @@ import './v1/voip/omnichannel';
 import './v1/voip';
 import './v1/federation';
 import './v1/moderation';
+import './default/openApi';
 
 export { API, APIClass, defaultRateLimiterOptions } from './api';
diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts
index 152e454b99ded..7e0ca4f091746 100644
--- a/apps/meteor/app/api/server/router.ts
+++ b/apps/meteor/app/api/server/router.ts
@@ -1,8 +1,42 @@
 import type { Method } from '@rocket.chat/rest-typings';
+import type { AnySchema } from 'ajv';
 import express from 'express';
 
 import type { TypedAction, TypedOptions } from './definition';
 
+export type Route = {
+	responses: Record<
+		number,
+		{
+			description: string;
+			content: {
+				'application/json': {
+					schema: AnySchema;
+				};
+			};
+		}
+	>;
+	parameters?: {
+		schema: AnySchema;
+		in: 'query';
+		name: 'query';
+		required: true;
+	}[];
+	requestBody?: {
+		required: true;
+		content: {
+			'application/json': {
+				schema: AnySchema;
+			};
+		};
+	};
+	security?: {
+		userId: [];
+		authToken: [];
+	}[];
+	tags?: string[];
+};
+
 export class Router<
 	TBasePath extends string,
 	TOperations extends {
@@ -13,7 +47,7 @@ export class Router<
 
 	constructor(readonly base: TBasePath) {}
 
-	public typedRoutes: Record<string, Record<string, unknown>> = {};
+	private typedRoutes: Record<string, Record<string, Route>> = {};
 
 	private registerTypedRoutes<
 		TSubPathPattern extends string,
@@ -86,6 +120,28 @@ export class Router<
 		this.middleware = (router: express.Router) => {
 			prev(router);
 			router[method.toLowerCase() as Lowercase<Method>](`/${subpath}`.replace('//', '/'), async (req, res) => {
+				if (options.query) {
+					const validatorFn = options.query;
+					if (typeof options.query === 'function' && !validatorFn(req.query)) {
+						return res.status(400).json({
+							success: false,
+							errorType: 'error-invalid-params',
+							error: validatorFn.errors?.map((error: any) => error.message).join('\n '),
+						});
+					}
+				}
+
+				if (options.body) {
+					const validatorFn = options.body;
+					if (typeof options.body === 'function' && !validatorFn((req as any).bodyParams || req.body)) {
+						return res.status(400).json({
+							success: false,
+							errorType: 'error-invalid-params',
+							error: validatorFn.errors?.map((error: any) => error.message).join('\n '),
+						});
+					}
+				}
+
 				const {
 					body,
 					statusCode = 200,
@@ -100,6 +156,17 @@ export class Router<
 					} as any,
 					[req],
 				);
+				if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) {
+					const responseValidatorFn = options?.response?.[statusCode];
+					if (!responseValidatorFn && options.typed) {
+						throw new Error(`Missing response validator for endpoint ${req.method} - ${req.url} with status code ${statusCode}`);
+					}
+					if (responseValidatorFn && !responseValidatorFn(body) && options.typed) {
+						throw new Error(
+							`Invalid response for endpoint ${req.method} - ${req.url}. Error: ${responseValidatorFn.errors?.map((error: any) => error.message).join('\n ')}`,
+						);
+					}
+				}
 
 				const responseHeaders = Object.fromEntries(
 					Object.entries({
diff --git a/apps/meteor/app/api/server/v1/custom-sounds.ts b/apps/meteor/app/api/server/v1/custom-sounds.ts
index ff144e08cfd43..e0d8a6d9675a9 100644
--- a/apps/meteor/app/api/server/v1/custom-sounds.ts
+++ b/apps/meteor/app/api/server/v1/custom-sounds.ts
@@ -1,39 +1,89 @@
 import { CustomSounds } from '@rocket.chat/models';
 import { isCustomSoundsListProps } from '@rocket.chat/rest-typings';
+import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv';
 import { escapeRegExp } from '@rocket.chat/string-helpers';
 
 import { API } from '../api';
 import { getPaginationItems } from '../helpers/getPaginationItems';
 
-API.v1.addRoute(
+API.v1.get(
 	'custom-sounds.list',
-	{ authRequired: true, validateParams: isCustomSoundsListProps },
 	{
-		async get() {
-			const { offset, count } = await getPaginationItems(this.queryParams as Record<string, string | number | null | undefined>);
-			const { sort, query } = await this.parseJsonQuery();
-
-			const { name } = this.queryParams;
+		response: {
+			200: ajv.compile({
+				additionalProperties: false,
+				type: 'object',
+				properties: {
+					count: {
+						type: 'number',
+						description: 'The number of sounds returned in this response.',
+					},
+					offset: {
+						type: 'number',
+						description: 'The number of sounds that were skipped in this response.',
+					},
+					total: {
+						type: 'number',
+						description: 'The total number of sounds that match the query.',
+					},
+					success: {
+						type: 'boolean',
+						description: 'Indicates if the request was successful.',
+					},
+					sounds: {
+						type: 'array',
+						items: {
+							type: 'object',
+							properties: {
+								_id: {
+									type: 'string',
+								},
+								name: {
+									type: 'string',
+								},
+								extension: {
+									type: 'string',
+								},
+								createdAt: {
+									type: 'string',
+								},
+								_updatedAt: {
+									type: 'object',
+								},
+							},
+							required: ['_id', 'name', 'extension', '_updatedAt'],
+						},
+					},
+				},
+				required: ['count', 'offset', 'total', 'sounds', 'success'],
+			}),
+		},
+		query: isCustomSoundsListProps,
+		authRequired: true,
+	},
+	async function action() {
+		const { offset, count } = await getPaginationItems(this.queryParams as Record<string, string | number | null | undefined>);
+		const { sort, query } = await this.parseJsonQuery();
 
-			const filter = {
-				...query,
-				...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}),
-			};
+		const { name } = this.queryParams;
 
-			const { cursor, totalCount } = CustomSounds.findPaginated(filter, {
-				sort: sort || { name: 1 },
-				skip: offset,
-				limit: count,
-			});
+		const filter = {
+			...query,
+			...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}),
+		};
 
-			const [sounds, total] = await Promise.all([cursor.toArray(), totalCount]);
+		const { cursor, totalCount } = CustomSounds.findPaginated(filter, {
+			sort: sort || { name: 1 },
+			skip: offset,
+			limit: count,
+		});
+		const [sounds, total] = await Promise.all([cursor.toArray(), totalCount]);
 
-			return API.v1.success({
-				sounds,
-				count: sounds.length,
-				offset,
-				total,
-			});
-		},
+		return API.v1.success({
+			sounds,
+			count: sounds.length,
+			offset,
+			total,
+		});
 	},
 );
diff --git a/apps/meteor/app/api/server/v1/federation.ts b/apps/meteor/app/api/server/v1/federation.ts
index 5f998546cf3e0..f5f9d1bc0e006 100644
--- a/apps/meteor/app/api/server/v1/federation.ts
+++ b/apps/meteor/app/api/server/v1/federation.ts
@@ -1,17 +1,39 @@
 import { Federation, FederationEE } from '@rocket.chat/core-services';
 import { License } from '@rocket.chat/license';
 import { isFederationVerifyMatrixIdProps } from '@rocket.chat/rest-typings';
+import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv';
 
 import { API } from '../api';
 
-API.v1.addRoute(
-	'federation/matrixIds.verify',
-	{
-		authRequired: true,
-		validateParams: isFederationVerifyMatrixIdProps,
-	},
-	{
-		async get() {
+API.v1
+	.get(
+		'federation/matrixIds.verify',
+		{
+			authRequired: true,
+			query: isFederationVerifyMatrixIdProps,
+			response: {
+				200: ajv.compile({
+					type: 'object',
+					properties: {
+						results: {
+							type: 'object',
+							patternProperties: {
+								'^[a-zA-Z0-9_-]+$': {
+									type: 'string',
+								},
+							},
+							additionalProperties: false,
+						},
+						success: {
+							type: 'boolean',
+							description: 'Indicates if the request was successful.',
+						},
+					},
+					required: ['results', 'success'],
+				}),
+			},
+		},
+		async function () {
 			const { matrixIds } = this.queryParams;
 
 			const federationService = License.hasValidLicense() ? FederationEE : Federation;
@@ -20,14 +42,40 @@ API.v1.addRoute(
 
 			return API.v1.success({ results: Object.fromEntries(results) });
 		},
-	},
-);
-
-API.v1.addRoute(
-	'federation/configuration.verify',
-	{ authRequired: true, permissionsRequired: ['view-privileged-setting'] },
-	{
-		async get() {
+	)
+	.get(
+		'federation/configuration.verify',
+		{
+			authRequired: true,
+			permissionsRequired: ['view-privileged-setting'],
+			response: {
+				200: ajv.compile({
+					type: 'object',
+					properties: {
+						externalReachability: { type: 'object', properties: { ok: { type: 'boolean' } }, required: ['ok'] },
+						appservice: { type: 'object', properties: { ok: { type: 'boolean' } }, required: ['ok'] },
+						success: {
+							type: 'boolean',
+							description: 'Indicates if the request was successful.',
+						},
+					},
+					required: ['externalReachability', 'appservice', 'success'],
+				}),
+				400: ajv.compile({
+					type: 'object',
+					properties: {
+						externalReachability: { type: 'object', properties: { ok: { type: 'boolean' } }, required: ['ok'] },
+						appservice: { type: 'object', properties: { ok: { type: 'boolean' } }, required: ['ok'] },
+						success: {
+							type: 'boolean',
+							description: 'Indicates if the request was successful.',
+						},
+					},
+					required: ['externalReachability', 'appservice', 'success'],
+				}),
+			},
+		},
+		async () => {
 			const service = License.hasValidLicense() ? FederationEE : Federation;
 
 			const status = await service.configurationStatus();
@@ -35,8 +83,6 @@ API.v1.addRoute(
 			if (!status.externalReachability.ok || !status.appservice.ok) {
 				return API.v1.failure(status);
 			}
-
 			return API.v1.success(status);
 		},
-	},
-);
+	);
diff --git a/apps/meteor/app/api/server/v1/invites.ts b/apps/meteor/app/api/server/v1/invites.ts
index ac916befc4fd0..76c920c31883c 100644
--- a/apps/meteor/app/api/server/v1/invites.ts
+++ b/apps/meteor/app/api/server/v1/invites.ts
@@ -5,6 +5,7 @@ import {
 	isValidateInviteTokenProps,
 	isSendInvitationEmailParams,
 } from '@rocket.chat/rest-typings';
+import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv';
 
 import { findOrCreateInvite } from '../../../invites/server/functions/findOrCreateInvite';
 import { listInvites } from '../../../invites/server/functions/listInvites';
@@ -14,33 +15,195 @@ import { useInviteToken } from '../../../invites/server/functions/useInviteToken
 import { validateInviteToken } from '../../../invites/server/functions/validateInviteToken';
 import { API } from '../api';
 
-API.v1.addRoute(
-	'listInvites',
-	{
-		authRequired: true,
-	},
-	{
-		async get() {
+API.v1
+	.get(
+		'listInvites',
+		{
+			authRequired: true,
+			response: {
+				200: ajv.compile({
+					additionalProperties: false,
+					type: 'array',
+					items: {
+						type: 'object',
+						properties: {
+							_id: {
+								type: 'string',
+							},
+							days: {
+								type: 'number',
+							},
+							rid: {
+								type: 'string',
+							},
+							userId: {
+								type: 'string',
+							},
+							createdAt: {
+								type: 'object',
+							},
+							expires: {
+								type: 'object',
+								nullable: true,
+							},
+							url: {
+								type: 'string',
+							},
+							_updatedAt: {
+								type: 'object',
+							},
+							maxUses: {
+								type: 'number',
+							},
+							uses: {
+								type: 'number',
+							},
+						},
+						required: ['_id', 'days', 'rid', 'userId', 'createdAt', 'url', '_updatedAt', 'maxUses', 'uses'],
+					},
+				}),
+				401: ajv.compile({
+					additionalProperties: false,
+					type: 'object',
+					properties: {
+						error: {
+							type: 'string',
+						},
+						status: {
+							type: 'string',
+							nullable: true,
+						},
+						message: {
+							type: 'string',
+							nullable: true,
+						},
+						success: {
+							type: 'boolean',
+							description: 'Indicates if the request was successful.',
+						},
+					},
+					required: ['success', 'error'],
+				}),
+			},
+		},
+
+		async function () {
 			const result = await listInvites(this.userId);
 			return API.v1.success(result);
 		},
-	},
-);
+	)
+	.post(
+		'findOrCreateInvite',
+		{
+			authRequired: true,
+			body: isFindOrCreateInviteParams,
+			response: {
+				200: ajv.compile({
+					additionalProperties: false,
+					type: 'object',
+					properties: {
+						_id: {
+							type: 'string',
+						},
+						rid: {
+							type: 'string',
+						},
+						userId: {
+							type: 'string',
+						},
+						createdAt: {
+							type: 'object',
+						},
+						_updatedAt: {
+							type: 'object',
+						},
+						expires: {
+							type: 'object',
+							nullable: true,
+						},
+						url: {
+							type: 'string',
+						},
+						maxUses: {
+							type: 'number',
+						},
+						days: {
+							type: 'number',
+						},
+						uses: {
+							type: 'number',
+						},
+						success: {
+							type: 'boolean',
+							description: 'Indicates if the request was successful.',
+						},
+					},
+					required: ['_id', 'rid', 'createdAt', 'maxUses', 'uses', 'userId', '_updatedAt', 'days', 'success'],
+				}),
+				400: ajv.compile({
+					additionalProperties: false,
+					type: 'object',
+					properties: {
+						error: {
+							type: 'string',
+						},
+						stack: {
+							type: 'string',
+							nullable: true,
+						},
+						errorType: {
+							type: 'string',
+						},
+						details: {
+							type: 'object',
+							nullable: true,
+							properties: {
+								rid: {
+									type: 'string',
+								},
+								method: {
+									type: 'string',
+								},
+							},
+						},
+						success: {
+							type: 'boolean',
+							description: 'Indicates if the request was successful.',
+						},
+					},
+					required: ['success', 'errorType', 'error'],
+				}),
+				401: ajv.compile({
+					additionalProperties: false,
+					type: 'object',
+					properties: {
+						error: {
+							type: 'string',
+						},
+						status: {
+							type: 'string',
+							nullable: true,
+						},
+						message: {
+							type: 'string',
+							nullable: true,
+						},
+						success: {
+							type: 'boolean',
+							description: 'Indicates if the request was successful.',
+						},
+					},
+					required: ['success', 'error'],
+				}),
+			},
+		},
 
-API.v1.addRoute(
-	'findOrCreateInvite',
-	{
-		authRequired: true,
-		validateParams: isFindOrCreateInviteParams,
-	},
-	{
-		async post() {
+		async function () {
 			const { rid, days, maxUses } = this.bodyParams;
 
 			return API.v1.success((await findOrCreateInvite(this.userId, { rid, days, maxUses })) as IInvite);
 		},
-	},
-);
+	);
 
 API.v1.addRoute(
 	'removeInvite/:_id',
diff --git a/apps/meteor/package.json b/apps/meteor/package.json
index 1597d59cf0639..351e41001b408 100644
--- a/apps/meteor/package.json
+++ b/apps/meteor/package.json
@@ -148,6 +148,7 @@
 		"@types/strict-uri-encode": "^2.0.2",
 		"@types/supertest": "^6.0.2",
 		"@types/supports-color": "~7.2.1",
+		"@types/swagger-ui-express": "^4.1.8",
 		"@types/textarea-caret": "^3.0.3",
 		"@types/ua-parser-js": "^0.7.39",
 		"@types/underscore": "^1.13.0",
@@ -425,6 +426,7 @@
 		"stream-buffers": "^3.0.3",
 		"strict-uri-encode": "^2.0.0",
 		"string-strip-html": "^8.5.0",
+		"swagger-ui-express": "^5.0.1",
 		"swiper": "patch:swiper@npm%3A11.1.14#~/.yarn/patches/swiper-npm-11.1.14-8126fa478a.patch",
 		"textarea-caret": "^3.1.0",
 		"tinykeys": "^1.4.0",
diff --git a/packages/rest-typings/src/default/index.ts b/packages/rest-typings/src/default/index.ts
index b3aa5d3aa535f..f24168e5c4792 100644
--- a/packages/rest-typings/src/default/index.ts
+++ b/packages/rest-typings/src/default/index.ts
@@ -1,3 +1,25 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+type OpenAPIJSONEndpoint = { withUndocumented?: boolean };
+
+const OpenAPIJSONEndpointSchema = {
+	type: 'object',
+	properties: {
+		withUndocumented: {
+			type: 'boolean',
+			nullable: true,
+		},
+	},
+	required: [],
+	additionalProperties: false,
+};
+
+export const isOpenAPIJSONEndpoint = ajv.compile<OpenAPIJSONEndpoint>(OpenAPIJSONEndpointSchema);
+
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export interface DefaultEndpoints {
 	'/info': {
@@ -35,4 +57,33 @@ export interface DefaultEndpoints {
 	'/ecdh_proxy/initEncryptedSession': {
 		POST: () => void;
 	};
+	'/docs/json': {
+		GET: (params: OpenAPIJSONEndpoint) => {
+			openapi: string;
+			info: {
+				title: string;
+				description: string;
+				version: string;
+			};
+			servers: {
+				url: string;
+			}[];
+			components: {
+				securitySchemes: {
+					userId: {
+						type: string;
+						in: string;
+						name: string;
+					};
+					authToken: {
+						type: string;
+						in: string;
+						name: string;
+					};
+				};
+				schemas: {};
+			};
+			paths: Record<string, Record<string, unknown>>;
+		};
+	};
 }
diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts
index fd1fc3ac2068b..c3d5721493a6c 100644
--- a/packages/rest-typings/src/index.ts
+++ b/packages/rest-typings/src/index.ts
@@ -274,3 +274,4 @@ export * from './v1/chat';
 export * from './v1/auth';
 export * from './v1/cloud';
 export * from './v1/banners';
+export * from './default';
diff --git a/yarn.lock b/yarn.lock
index 48906dff3e98e..c4d3781bcef2e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8936,6 +8936,7 @@ __metadata:
     "@types/strict-uri-encode": "npm:^2.0.2"
     "@types/supertest": "npm:^6.0.2"
     "@types/supports-color": "npm:~7.2.1"
+    "@types/swagger-ui-express": "npm:^4.1.8"
     "@types/textarea-caret": "npm:^3.0.3"
     "@types/ua-parser-js": "npm:^0.7.39"
     "@types/underscore": "npm:^1.13.0"
@@ -9127,6 +9128,7 @@ __metadata:
     stylelint-selector-bem-pattern: "npm:^4.0.1"
     supertest: "npm:^7.0.0"
     supports-color: "npm:~7.2.0"
+    swagger-ui-express: "npm:^5.0.1"
     swiper: "patch:swiper@npm%3A11.1.14#~/.yarn/patches/swiper-npm-11.1.14-8126fa478a.patch"
     template-file: "npm:^6.0.1"
     textarea-caret: "npm:^3.1.0"
@@ -10254,6 +10256,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@scarf/scarf@npm:=1.4.0":
+  version: 1.4.0
+  resolution: "@scarf/scarf@npm:1.4.0"
+  checksum: 10/1b39a18fa29e91cfbc134c588e20c5f01a1b21ec4473614123801155b48378e9c3bf72adaca8c67e433ae951ab653268e9502cc5733230d8927532f74a6b89c9
+  languageName: node
+  linkType: hard
+
 "@selderee/plugin-htmlparser2@npm:^0.11.0":
   version: 0.11.0
   resolution: "@selderee/plugin-htmlparser2@npm:0.11.0"
@@ -13128,6 +13137,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/swagger-ui-express@npm:^4.1.8":
+  version: 4.1.8
+  resolution: "@types/swagger-ui-express@npm:4.1.8"
+  dependencies:
+    "@types/express": "npm:*"
+    "@types/serve-static": "npm:*"
+  checksum: 10/bf4d84fb21c11e820238685cdc102f8d5e82d4382df9450a358bf80f70f5f0699c87645a291c52beee1eba5c0ee594e03714b236f1a0737272941a8708218f8f
+  languageName: node
+  linkType: hard
+
 "@types/tern@npm:*":
   version: 0.23.4
   resolution: "@types/tern@npm:0.23.4"
@@ -34861,6 +34880,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"swagger-ui-dist@npm:>=5.0.0":
+  version: 5.20.1
+  resolution: "swagger-ui-dist@npm:5.20.1"
+  dependencies:
+    "@scarf/scarf": "npm:=1.4.0"
+  checksum: 10/6bd82f8bf2bc6d5ba062c1e3d3cb0ef057403784afb009ff0a582f59b14a24c5c8e8b3c4541251d1562f259e6a8ffca42ac9d12714691b2b35a1b35f56df2489
+  languageName: node
+  linkType: hard
+
+"swagger-ui-express@npm:^5.0.1":
+  version: 5.0.1
+  resolution: "swagger-ui-express@npm:5.0.1"
+  dependencies:
+    swagger-ui-dist: "npm:>=5.0.0"
+  peerDependencies:
+    express: ">=4.0.0 || >=5.0.0-beta"
+  checksum: 10/13d95e6683214717f98dd53063bf398751ea798e971c43ece949b67a45102cc48995da319547002b2acc262d39f1994bd4af39828d99e95658966d6b332b585e
+  languageName: node
+  linkType: hard
+
 "swiper@npm:11.1.14":
   version: 11.1.14
   resolution: "swiper@npm:11.1.14"


Reviews:
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T20:18:05Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T20:18:07Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T20:18:11Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T20:18:13Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T20:18:19Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T20:18:23Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T20:18:26Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T20:18:29Z
user: login: MarcosSpessatto
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T18:30:57Z
user: login: ggazzo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T18:54:28Z
user: login: ggazzo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T18:56:05Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-12T22:40:37Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-12T22:40:52Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-12T22:40:55Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-12T22:40:58Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-12T22:41:04Z
user: login: MarcosSpessatto
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T15:32:34Z
user: login: MarcosSpessatto
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T12:14:36Z

Review comments:
diff_hunk: @@ -1058,19 +1249,15 @@ const defaultOptionsEndpoint = async function _defaultOptionsEndpoint(this: Rest
 	this.done();
 };
 
-const createApi = function _createApi(options: { version?: string } = {}): APIClass {
-	return new APIClass(
-		Object.assign(
-			{
-				apiPath: 'api/',
-				useDefaultAuth: true,
-				prettyJson: process.env.NODE_ENV === 'development',
-				defaultOptionsEndpoint,
-				auth: getUserAuth(),
-			},
-			options,
-		) as IAPIProperties,
-	);
+const createApi = function _createApi(options: { version?: string; useDefaultAuth?: true } = {}): APIClass {
+	return new APIClass({
+		apiPath: 'api/',
+		useDefaultAuth: false,
+		prettyJson: process.env.NODE_ENV === 'development',
+		defaultOptionsEndpoint,
+		auth: getUserAuth(),
+		...options,
+	});
 };
path: apps/meteor/app/api/server/api.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Potential Issues](https://img.shields.io/badge/Potential_Issues-B71C1C)

```typescript
interface ApiOptions {
  version?: string;
  useDefaultAuth?: true;
}

const validateApiOptions = (options: ApiOptions): void => {
  if (options.version && typeof options.version !== 'string') {
    throw new Meteor.Error('error-invalid-api-version', 'API version must be a string');
  }
};

const createApi = function _createApi(options: ApiOptions = {}): APIClass {
  validateApiOptions(options);
  return new APIClass({
    apiPath: 'api/',
    useDefaultAuth: false,
    prettyJson: process.env.NODE_ENV === 'development',
    defaultOptionsEndpoint,
    auth: getUserAuth(),
    ...options,
  });
};
```
Potential runtime errors due to loose options type

This issue appears in multiple locations:
* apps/meteor/app/api/server/api.ts: Lines 1252-1261



Add type validation or constraints for the options parameter



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-02-28T20:18:04Z
author_association: NONE
diff_hunk: @@ -484,6 +495,182 @@ export class APIClass<TBasePath extends string = ''> extends Restivus {
 		return routeActions.map((action) => this.getFullRouteName(route, action, apiVersion));
 	}
 
+	private registerTypedRoutesLegacy<TSubPathPattern extends string, TOptions extends Options>(
+		method: Method,
+		subpath: TSubPathPattern,
+		options: TOptions,
+	): void {
+		const { authRequired, validateParams } = options;
+
+		const opt = {
+			authRequired,
+			...(validateParams &&
+				method.toLowerCase() === 'get' &&
+				('GET' in validateParams
+					? { query: validateParams.GET }
+					: {
+							query: validateParams as ValidateFunction<any>,
+						})),
+
+			...(validateParams &&
+				method.toLowerCase() === 'post' &&
+				('POST' in validateParams ? { query: validateParams.POST } : { body: validateParams as ValidateFunction<any> })),
+
+			...(validateParams &&
+				method.toLowerCase() === 'put' &&
+				('PUT' in validateParams ? { query: validateParams.PUT } : { body: validateParams as ValidateFunction<any> })),
+			...(validateParams &&
+				method.toLowerCase() === 'delete' &&
+				('DELETE' in validateParams ? { query: validateParams.DELETE } : { body: validateParams as ValidateFunction<any> })),
+
+			tags: ['Missing Documentation'],
+			response: {
+				200: ajv.compile({
+					type: 'object',
+					properties: {
+						success: { type: 'boolean' },
+						error: { type: 'string' },
+					},
+					required: ['success'],
+				}),
+			},
+		};
+
+		this.registerTypedRoutes(method, subpath, opt);
+	}
+
+	private registerTypedRoutes<
+		TSubPathPattern extends string,
+		TOptions extends TypedOptions,
+		TPathPattern extends `${TBasePath}/${TSubPathPattern}`,
+	>(method: Method, subpath: TSubPathPattern, options: TOptions): void {
+		const path = `/${this._config.apiPath}/${subpath}`.replaceAll('//', '/') as TPathPattern;
+		this.typedRoutes = this.typedRoutes || {};
+		this.typedRoutes[path] = this.typedRoutes[subpath] || {};
path: apps/meteor/app/api/server/api.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Error Handling](https://img.shields.io/badge/Error_Handling-FF6D00)

```typescript
try {
  this.typedRoutes = this.typedRoutes || {};
  if (typeof path !== 'string' || typeof method !== 'string') {
    throw new Error('Invalid path or method for route registration');
  }
  this.typedRoutes[path] = this.typedRoutes[path] || {};
  this.typedRoutes[path][method.toLowerCase()] = this.typedRoutes[path][method.toLowerCase()] || {};
} catch (error) {
  logger.error(`Failed to register typed route: ${error.message}`);
  throw new Meteor.Error('error-route-registration', error.message);
}
```
Lack of error handling and input validation during route registration

This issue appears in multiple locations:
* apps/meteor/app/api/server/api.ts: Lines 548-549



Add try-catch blocks and validation to handle potential errors during route registration



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-02-28T20:18:07Z
author_association: NONE
diff_hunk: @@ -0,0 +1,45 @@
+import { API } from './api';
+import { settings } from '../../settings/server';
+import { Info } from '../../utils/rocketchat.info';
+
+const openApi = new API.ApiClass({
+	version: 'json',
+	useDefaultAuth: false,
+	prettyJson: false,
+	enableCors: false,
+	auth: API.getUserAuth(),
+});
+
+openApi.addRoute('/', {
+	get() {
+		return {
+			openapi: '3.0.3',
+			info: {
+				title: 'Rocket.Chat API',
+				description: 'Rocket.Chat API',
+				version: Info.version,
+			},
+			servers: [
+				{
+					url: settings.get('Site_Url'),
path: apps/meteor/app/api/server/openApi.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Error Handling](https://img.shields.io/badge/Error_Handling-FF6D00)

```typescript
url: String(settings.get('Site_Url') || ''),
```
Potential errors due to missing null check and type assertion

This issue appears in multiple locations:
* apps/meteor/app/api/server/openApi.ts: Lines 24-24



Add null check and ensure the value is a string for the URL



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-02-28T20:18:10Z
author_association: NONE
diff_hunk: @@ -1,39 +1,88 @@
 import { CustomSounds } from '@rocket.chat/models';
 import { isCustomSoundsListProps } from '@rocket.chat/rest-typings';
+import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv';
 import { escapeRegExp } from '@rocket.chat/string-helpers';
 
 import { API } from '../api';
+import type { ExtractEndpoints } from '../definition';
 import { getPaginationItems } from '../helpers/getPaginationItems';
 
-API.v1.addRoute(
+const customSounds = API.v1.get(
 	'custom-sounds.list',
-	{ authRequired: true, validateParams: isCustomSoundsListProps },
 	{
-		async get() {
-			const { offset, count } = await getPaginationItems(this.queryParams as Record<string, string | number | null | undefined>);
-			const { sort, query } = await this.parseJsonQuery();
-
-			const { name } = this.queryParams;
+		response: {
+			200: ajv.compile({
+				additionalProperties: false,
+				type: 'object',
+				properties: {
+					count: {
+						type: 'number',
+						description: 'The number of sounds returned in this response.',
+					},
+					offset: {
+						type: 'number',
+						description: 'The number of sounds that were skipped in this response.',
+					},
+					total: {
+						type: 'number',
+						description: 'The total number of sounds that match the query.',
+					},
+					sounds: {
+						type: 'array',
+						items: {
+							type: 'object',
+							properties: {
+								_id: {
+									type: 'string',
+								},
+								name: {
+									type: 'string',
+								},
+								extension: {
+									type: 'string',
+								},
+								createdAt: {
+									type: 'string',
+								},
+								updatedAt: {
+									type: 'string',
+								},
+							},
+							required: ['_id', 'name', 'extension', 'createdAt', 'updatedAt'],
+						},
+					},
+				},
+				required: ['count', 'offset', 'total', 'sounds'],
+			}),
+		},
+		query: isCustomSoundsListProps,
+		authRequired: true,
+	},
+	async function action() {
+		const { offset, count } = await getPaginationItems(this.queryParams as Record<string, string | number | null | undefined>);
+		const { sort, query } = await this.parseJsonQuery();
 
-			const filter = {
-				...query,
-				...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}),
-			};
+		const { name } = this.queryParams;
 
-			const { cursor, totalCount } = CustomSounds.findPaginated(filter, {
-				sort: sort || { name: 1 },
-				skip: offset,
-				limit: count,
-			});
+		const filter = {
+			...query,
+			...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}),
+		};
 
-			const [sounds, total] = await Promise.all([cursor.toArray(), totalCount]);
+		const { cursor, totalCount } = CustomSounds.findPaginated(filter, {
+			sort: sort || { name: 1 },
+			skip: offset,
+			limit: count,
+		});
+		const [sounds, total] = await Promise.all([cursor.toArray(), totalCount]);
path: apps/meteor/app/api/server/v1/custom-sounds.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Error Handling](https://img.shields.io/badge/Error_Handling-FF6D00)

```typescript
let sounds: any[] = [];
let total = 0;
try {
  [sounds, total] = await Promise.all([cursor.toArray(), totalCount]);
} catch (error) {
  console.error('Failed to fetch custom sounds:', error);
  return API.v1.failure('Failed to fetch custom sounds');
}
return API.v1.success({
  sounds,
  count: sounds.length,
  offset,
  total,
});
```
Potential unhandled errors due to missing error handling in Promise.all

This issue appears in multiple locations:
* apps/meteor/app/api/server/v1/custom-sounds.ts: Lines 77-77



Add try-catch block to handle potential failures in Promise.all operation



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-02-28T20:18:13Z
author_association: NONE
diff_hunk: @@ -20,23 +38,39 @@ API.v1.addRoute(
 
 			return API.v1.success({ results: Object.fromEntries(results) });
 		},
-	},
-);
-
-API.v1.addRoute(
-	'federation/configuration.verify',
-	{ authRequired: true, permissionsRequired: ['view-privileged-setting'] },
-	{
-		async get() {
+	)
+	.get(
+		'federation/configuration.verify',
+		{
+			authRequired: true,
+			permissionsRequired: ['view-privileged-setting'],
+			response: {
+				200: ajv.compile({
path: apps/meteor/app/api/server/v1/federation.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Maintainability](https://img.shields.io/badge/Maintainability-0091EA)

```typescript
const configurationVerifySchema = {    type: 'object',    properties: {        externalReachability: { type: 'object', properties: { ok: { type: 'boolean' } }, required: ['ok'] },        appservice: { type: 'object', properties: { ok: { type: 'boolean' } }, required: ['ok'] },    },    required: ['externalReachability', 'appservice'],};
response: {
    200: ajv.compile(configurationVerifySchema),
    400: ajv.compile(configurationVerifySchema)
}
```
Code duplication in schema definition

This issue appears in multiple locations:
* apps/meteor/app/api/server/v1/federation.ts: Lines 48-64



Extract duplicate schema definition into a reusable constant



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-02-28T20:18:19Z
author_association: NONE
diff_hunk: @@ -14,33 +15,94 @@ import { useInviteToken } from '../../../invites/server/functions/useInviteToken
 import { validateInviteToken } from '../../../invites/server/functions/validateInviteToken';
 import { API } from '../api';
 
-API.v1.addRoute(
-	'listInvites',
-	{
-		authRequired: true,
-	},
-	{
-		async get() {
+API.v1
+	.get(
+		'listInvites',
+		{
+			authRequired: true,
+			response: {
+				200: ajv.compile({
path: apps/meteor/app/api/server/v1/invites.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Maintainability](https://img.shields.io/badge/Maintainability-0091EA)

```typescript
const inviteSchema = {
  type: 'object',
  properties: {
    _id: { type: 'string' },
    rid: { type: 'string' },
    createdAt: { type: 'string' },
    expireAt: { type: 'string' },
    maxUses: { type: 'number' },
    uses: { type: 'number' },
  },
  required: ['_id', 'rid', 'createdAt', 'expireAt', 'maxUses', 'uses']
};

// In listInvites response schema:
invites: {
  type: 'array',
  items: inviteSchema
}

// In findOrCreateInvite response schema:
200: ajv.compile(inviteSchema)
```
Code duplication in invite object schema

This issue appears in multiple locations:
* apps/meteor/app/api/server/v1/invites.ts: Lines 24-96



Extract duplicate invite schema definition into a reusable constant



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-02-28T20:18:23Z
author_association: NONE
diff_hunk: @@ -484,6 +495,182 @@ export class APIClass<TBasePath extends string = ''> extends Restivus {
 		return routeActions.map((action) => this.getFullRouteName(route, action, apiVersion));
 	}
 
+	private registerTypedRoutesLegacy<TSubPathPattern extends string, TOptions extends Options>(
+		method: Method,
+		subpath: TSubPathPattern,
+		options: TOptions,
+	): void {
+		const { authRequired, validateParams } = options;
+
+		const opt = {
+			authRequired,
+			...(validateParams &&
path: apps/meteor/app/api/server/api.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Refactoring](https://img.shields.io/badge/Refactoring-304FFE)

```typescript
const getValidationConfig = (method: string, validateParams: any) => {
  const key = method.toUpperCase();
  const paramType = ['GET', 'DELETE'].includes(key) ? 'query' : 'body';
  return key in validateParams
    ? { [paramType]: validateParams[key] }
    : { [paramType]: validateParams as ValidateFunction<any> };
};

// Usage in registerTypedRoutesLegacy:
...(validateParams && getValidationConfig(method, validateParams)),
```
Code duplication in validation logic

This issue appears in multiple locations:
* apps/meteor/app/api/server/api.ts: Lines 507-524
* apps/meteor/app/api/server/definition.ts: Lines 258-272



Extract repetitive validation logic into a separate helper function



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-02-28T20:18:25Z
author_association: NONE
diff_hunk: @@ -202,6 +203,74 @@ type Operation<TMethod extends Method, TPathPattern extends PathPattern, TEndpoi
 			action: Action<TMethod, TPathPattern, TEndpointOptions>;
 	  } & { twoFactorRequired: boolean });
 
+type ActionOperation<TMethod extends Method, TPathPattern extends PathPattern, TEndpointOptions extends Options = object> = {
+	action: Action<TMethod, TPathPattern, TEndpointOptions>;
+} & TEndpointOptions;
+
 export type Operations<TPathPattern extends PathPattern, TOptions extends Options = object> = {
 	[M in MethodOf<TPathPattern> as Lowercase<M>]: Operation<Uppercase<M>, TPathPattern, TOptions>;
 };
+
+export type ActionOperations<TPathPattern extends PathPattern, TOptions extends Options = object> = {
+	[M in MethodOf<TPathPattern> as Lowercase<M>]: ActionOperation<Uppercase<M>, TPathPattern, TOptions>;
+};
+
+type Prettify<T> = {
+	[K in keyof T]: T[K];
+} & {};
+
+export type TypedOptions = {
+	response: {
+		200: ValidateFunction;
+		300?: ValidateFunction;
+		400?: ValidateFunction;
+		401?: ValidateFunction;
+		403?: ValidateFunction;
+		404?: ValidateFunction;
+		500?: ValidateFunction;
+	};
+	query?: ValidateFunction;
+	body?: ValidateFunction;
+	tags?: string[];
+} & Options;
+
path: apps/meteor/app/api/server/definition.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Documentation and Comments](https://img.shields.io/badge/Documentation_and_Comments-D81B60)

```typescript
/**
 * Configuration options for typed API endpoints
 * @property response - Validation functions for different HTTP status codes
 * @property query - Validation function for query parameters
 * @property body - Validation function for request body
 * @property tags - API endpoint tags for documentation/categorization
 */
export type TypedOptions = {
    response: {
        200: ValidateFunction;
        300?: ValidateFunction;
        400?: ValidateFunction;
        401?: ValidateFunction;
        403?: ValidateFunction;
        404?: ValidateFunction;
        500?: ValidateFunction;
    };
    query?: ValidateFunction;
    body?: ValidateFunction;
    tags?: string[];
} & Options;
```
Missing JSDoc comments for TypedOptions type

This issue appears in multiple locations:
* apps/meteor/app/api/server/definition.ts: Lines 222-236



Add JSDoc comments to explain the purpose and usage of the TypedOptions type



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-02-28T20:18:29Z
author_association: NONE
diff_hunk: @@ -0,0 +1,90 @@
+import { isOpenAPIJSONEndpoint } from '@rocket.chat/rest-typings';
+import express from 'express';
+import { WebApp } from 'meteor/webapp';
+import swaggerUi from 'swagger-ui-express';
+
+import { settings } from '../../../settings/server';
+import { Info } from '../../../utils/rocketchat.info';
+import { API } from '../api';
+import type { Route } from '../router';
+
+const app = express();
+
+const getTypedRoutes = (
+	typedRoutes: Record<string, Record<string, Route>>,
+	{ withUndocumented = false }: { withUndocumented?: boolean } = {},
+): Record<string, Record<string, Route>> => {
+	if (withUndocumented) {
+		return typedRoutes;
+	}
+
+	return Object.entries(typedRoutes).reduce(
+		(acc, [path, methods]) => {
+			const filteredMethods = Object.entries(methods)
+				.filter(([_, options]) => !options?.tags?.includes('Missing Documentation'))
+				.reduce(
+					(acc, [method, options]) => {
+						acc[method] = options;
+						return acc;
+					},
+					{} as Record<string, Route>,
+				);
+
+			if (Object.keys(filteredMethods).length > 0) {
+				acc[path] = filteredMethods;
+			}
+
+			return acc;
+		},
+		{} as Record<string, Record<string, Route>>,
+	);
+};
+
+const makeOpenAPIResponse = (paths: Record<string, Record<string, Route>>) => ({
+	openapi: '3.0.3',
+	info: {
+		title: 'Rocket.Chat API',
+		description: 'Rocket.Chat API',
+		version: Info.version,
+	},
+	servers: [
+		{
+			url: settings.get('Site_Url'),
+		},
+	],
+	components: {
+		securitySchemes: {
+			userId: {
+				type: 'apiKey',
+				in: 'header',
+				name: 'X-User-Id',
+			},
+			authToken: {
+				type: 'apiKey',
+				in: 'header',
+				name: 'X-Auth-Token',
+			},
+		},
+		schemas: {},
+	},
+	paths,
+});
+
+API.default.addRoute(
+	'docs/json',
+	{ authRequired: false, validateParams: isOpenAPIJSONEndpoint },
+	{
+		get() {
+			const { withUndocumented = false } = this.queryParams;
+
+			return API.default.success(makeOpenAPIResponse(getTypedRoutes(API.v1.typedRoutes, { withUndocumented })));
+		},
+	},
+);
+
+app.use(
path: apps/meteor/app/api/server/default/openApi.ts
user: login: MarcosSpessatto
body: @ggazzo do you think this should be enabled by default, or should we allow it to be through an env var or setting?
created_at: 2025-03-12T18:22:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,6 +156,15 @@ export class Router<
 					} as any,
 					[req],
 				);
+				if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) {
+					const responseValidatorFn = options?.response?.[statusCode];
+					if (!responseValidatorFn) {
+						console.warn(`Missing response validator for endpoint ${req.method} - ${req.url} with status code ${statusCode}`);
path: apps/meteor/app/api/server/router.ts
user: login: MarcosSpessatto
body: @ggazzo do you think this is useful, or should we remove it to not flood the logs?
created_at: 2025-03-12T18:23:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,6 +156,15 @@ export class Router<
 					} as any,
 					[req],
 				);
+				if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) {
+					const responseValidatorFn = options?.response?.[statusCode];
+					if (!responseValidatorFn) {
+						console.warn(`Missing response validator for endpoint ${req.method} - ${req.url} with status code ${statusCode}`);
path: apps/meteor/app/api/server/router.ts
user: login: ggazzo
body: I think we should throw an error actually we should not allow wrong response payloads right?
created_at: 2025-03-12T18:54:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,90 @@
+import { isOpenAPIJSONEndpoint } from '@rocket.chat/rest-typings';
+import express from 'express';
+import { WebApp } from 'meteor/webapp';
+import swaggerUi from 'swagger-ui-express';
+
+import { settings } from '../../../settings/server';
+import { Info } from '../../../utils/rocketchat.info';
+import { API } from '../api';
+import type { Route } from '../router';
+
+const app = express();
+
+const getTypedRoutes = (
+	typedRoutes: Record<string, Record<string, Route>>,
+	{ withUndocumented = false }: { withUndocumented?: boolean } = {},
+): Record<string, Record<string, Route>> => {
+	if (withUndocumented) {
+		return typedRoutes;
+	}
+
+	return Object.entries(typedRoutes).reduce(
+		(acc, [path, methods]) => {
+			const filteredMethods = Object.entries(methods)
+				.filter(([_, options]) => !options?.tags?.includes('Missing Documentation'))
+				.reduce(
+					(acc, [method, options]) => {
+						acc[method] = options;
+						return acc;
+					},
+					{} as Record<string, Route>,
+				);
+
+			if (Object.keys(filteredMethods).length > 0) {
+				acc[path] = filteredMethods;
+			}
+
+			return acc;
+		},
+		{} as Record<string, Record<string, Route>>,
+	);
+};
+
+const makeOpenAPIResponse = (paths: Record<string, Record<string, Route>>) => ({
+	openapi: '3.0.3',
+	info: {
+		title: 'Rocket.Chat API',
+		description: 'Rocket.Chat API',
+		version: Info.version,
+	},
+	servers: [
+		{
+			url: settings.get('Site_Url'),
+		},
+	],
+	components: {
+		securitySchemes: {
+			userId: {
+				type: 'apiKey',
+				in: 'header',
+				name: 'X-User-Id',
+			},
+			authToken: {
+				type: 'apiKey',
+				in: 'header',
+				name: 'X-Auth-Token',
+			},
+		},
+		schemas: {},
+	},
+	paths,
+});
+
+API.default.addRoute(
+	'docs/json',
+	{ authRequired: false, validateParams: isOpenAPIJSONEndpoint },
+	{
+		get() {
+			const { withUndocumented = false } = this.queryParams;
+
+			return API.default.success(makeOpenAPIResponse(getTypedRoutes(API.v1.typedRoutes, { withUndocumented })));
+		},
+	},
+);
+
+app.use(
path: apps/meteor/app/api/server/default/openApi.ts
user: login: ggazzo
body: I think its ok so far
created_at: 2025-03-12T18:56:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,90 @@
+import { isOpenAPIJSONEndpoint } from '@rocket.chat/rest-typings';
+import express from 'express';
+import { WebApp } from 'meteor/webapp';
+import swaggerUi from 'swagger-ui-express';
+
+import { settings } from '../../../settings/server';
+import { Info } from '../../../utils/rocketchat.info';
+import { API } from '../api';
+import type { Route } from '../router';
+
+const app = express();
path: apps/meteor/app/api/server/default/openApi.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Security](https://img.shields.io/badge/Security-D50000)

```typescript
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';

const app = express();
app.use(helmet());
app.use(rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 500 // higher limit for API docs
}));
```
Lack of security measures such as input validation, origin checks, and security middleware, increasing the risk of security vulnerabilities.

This issue appears in multiple locations:
* apps/meteor/app/api/server/default/openApi.ts: Lines 11-11
* apps/meteor/client/lib/utils/fireGlobalEvent.ts: Lines 17-17
* apps/meteor/client/lib/utils/fireGlobalEventBase.ts: Lines 8-14



Please add security measures such as input validation, origin checks, and security middleware to protect against potential security threats.



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-03-12T22:40:37Z
author_association: NONE
diff_hunk: @@ -0,0 +1,90 @@
+import { isOpenAPIJSONEndpoint } from '@rocket.chat/rest-typings';
+import express from 'express';
+import { WebApp } from 'meteor/webapp';
+import swaggerUi from 'swagger-ui-express';
+
+import { settings } from '../../../settings/server';
+import { Info } from '../../../utils/rocketchat.info';
+import { API } from '../api';
+import type { Route } from '../router';
+
+const app = express();
path: apps/meteor/app/api/server/default/openApi.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Error Handling](https://img.shields.io/badge/Error_Handling-FF6D00)

```typescript
const getTypedRoutes = (
	typedRoutes: Record<string, Record<string, Route>>,
	{ withUndocumented = false }: { withUndocumented?: boolean } = {},
): Record<string, Record<string, Route>> => {
	if (!typedRoutes || typeof typedRoutes !== 'object') {
		throw new Error('Invalid typedRoutes: Expected a non-null object with Route records');
	}
```
Add error handling for potential undefined or malformed `typedRoutes` input to prevent runtime errors.



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-03-12T22:40:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,90 @@
+import { isOpenAPIJSONEndpoint } from '@rocket.chat/rest-typings';
+import express from 'express';
+import { WebApp } from 'meteor/webapp';
+import swaggerUi from 'swagger-ui-express';
+
+import { settings } from '../../../settings/server';
+import { Info } from '../../../utils/rocketchat.info';
+import { API } from '../api';
+import type { Route } from '../router';
+
+const app = express();
+
+const getTypedRoutes = (
+	typedRoutes: Record<string, Record<string, Route>>,
+	{ withUndocumented = false }: { withUndocumented?: boolean } = {},
+): Record<string, Record<string, Route>> => {
+	if (withUndocumented) {
+		return typedRoutes;
+	}
+
+	return Object.entries(typedRoutes).reduce(
+		(acc, [path, methods]) => {
+			const filteredMethods = Object.entries(methods)
+				.filter(([_, options]) => !options?.tags?.includes('Missing Documentation'))
+				.reduce(
+					(acc, [method, options]) => {
+						acc[method] = options;
+						return acc;
+					},
+					{} as Record<string, Route>,
+				);
+
+			if (Object.keys(filteredMethods).length > 0) {
+				acc[path] = filteredMethods;
+			}
+
+			return acc;
+		},
+		{} as Record<string, Record<string, Route>>,
+	);
+};
+
+const makeOpenAPIResponse = (paths: Record<string, Record<string, Route>>) => ({
path: apps/meteor/app/api/server/default/openApi.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Maintainability](https://img.shields.io/badge/Maintainability-0091EA)

```typescript
const BASE_OPENAPI_CONFIG = {
	openapi: '3.0.3',
	info: {
		title: 'Rocket.Chat API',
		description: 'Rocket.Chat API',
		version: Info.version,
	},
	components: {
		securitySchemes: {
			userId: {
				type: 'apiKey',
				in: 'header',
				name: 'X-User-Id',
			},
			authToken: {
				type: 'apiKey',
				in: 'header',
				name: 'X-Auth-Token',
			},
		},
		schemas: {},
	},
};

const makeOpenAPIResponse = (paths: Record<string, Record<string, Route>>) => ({
	...BASE_OPENAPI_CONFIG,
	servers: [{ url: settings.get('Site_Url') }],
	paths,
});
```
Extract the OpenAPI configuration object into a separate constant to improve maintainability and reusability



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-03-12T22:40:55Z
author_association: NONE
diff_hunk: @@ -100,6 +156,15 @@ export class Router<
 					} as any,
 					[req],
 				);
+				if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) {
+					const responseValidatorFn = options?.response?.[statusCode];
path: apps/meteor/app/api/server/router.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Error Handling](https://img.shields.io/badge/Error_Handling-FF6D00)

```typescript
const responseValidatorFn = options?.response?.[statusCode];
```
The change introduces optional chaining when accessing the response validator function, which is a good practice to prevent runtime errors when the response object is undefined.  This is great for handling potentially missing 'response' or incorrect status codes. To further enhance type safety and clarity, consider adding a check to ensure the 'options.response' exists and has the expected structure before accessing properties like statusCode.



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-03-12T22:40:58Z
author_association: NONE
diff_hunk: @@ -35,4 +57,33 @@ export interface DefaultEndpoints {
 	'/ecdh_proxy/initEncryptedSession': {
 		POST: () => void;
 	};
+	'/docs/json': {
+		GET: (params: OpenAPIJSONEndpoint) => {
path: packages/rest-typings/src/default/index.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-brightgreen)![Error Handling](https://img.shields.io/badge/Error_Handling-FF6D00)

```typescript
type OpenAPIJSONResponse = {
    openapi: string;
    info: {
        title: string;
        description: string;
        version: string;
    };
    servers: {
        url: string;
    }[];
    components: {
        securitySchemes: {
            userId: {
                type: string;
                in: string;
                name: string;
            };
            authToken: {
                type: string;
                in: string;
                name: string;
            };
        };
        schemas: {};
    };
    paths: Record<string, Record<string, unknown>>;
};

const OpenAPIJSONResponseSchema = {
    type: 'object',
    properties: {
        openapi: { type: 'string' },
        info: {
            type: 'object',
            properties: {
                title: { type: 'string' },
                description: { type: 'string' },
                version: { type: 'string' }
            },
            required: ['title', 'description', 'version']
        },
        servers: {
            type: 'array',
            items: {
                type: 'object',
                properties: {
                    url: { type: 'string' }
                },
                required: ['url']
            }
        },
        components: {
            type: 'object',
            properties: {
                securitySchemes: {
                    type: 'object',
                    properties: {
                        userId: {
                            type: 'object',
                            properties: {
                                type: { type: 'string' },
                                in: { type: 'string' },
                                name: { type: 'string' }
                            },
                            required: ['type', 'in', 'name']
                        },
                        authToken: {
                            type: 'object',
                            properties: {
                                type: { type: 'string' },
                                in: { type: 'string' },
                                name: { type: 'string' }
                            },
                            required: ['type', 'in', 'name']
                        }
                    },
                    required: ['userId', 'authToken']
                },
                schemas: { type: 'object' }
            },
            required: ['securitySchemes', 'schemas']
        },
        paths: { type: 'object' }
    },
    required: ['openapi', 'info', 'servers', 'components', 'paths'],
    additionalProperties: false
};

export const isValidOpenAPIJSONResponse = ajv.compile<OpenAPIJSONResponse>(OpenAPIJSONResponseSchema);

// ...
GET: (params: OpenAPIJSONEndpoint): OpenAPIJSONResponse => { /* ... */ }
```
Consider adding input validation for the OpenAPI response schema using a separate validator function, similar to how the input parameters are validated. This ensures API response consistency and helps catch potential issues during development.



<sub>Talk to Kody by mentioning @kody</sub>

<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub><!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-03-12T22:41:04Z
author_association: NONE

PR comments:
user: login: dionisio-bot[bot]
created_at: 2025-01-21T03:14:49Z
body: Looks like this PR is not ready to merge, because of the following issues:
- This PR is missing the 'stat: QA assured' label

Please fix the issues and try again

If you have any trouble, please check the [PR guidelines](https://handbook.rocket.chat/departments-and-operations/research-and-development/engineering/development/pr-general-instructions-and-handling)
author_association: CONTRIBUTOR
user: login: changeset-bot[bot]
created_at: 2025-01-21T03:14:50Z
body: ###  🦋  Changeset detected

Latest commit: 64f803bdb68e2c524234782b14cf5332f3ebf368

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 35 packages</summary>

  | Name                                | Type  |
| ----------------------------------- | ----- |
| @rocket.chat/meteor                 | Minor |
| @rocket.chat/rest-typings           | Minor |
| @rocket.chat/api-client             | Patch |
| @rocket.chat/core-services          | Patch |
| @rocket.chat/ddp-client             | Patch |
| @rocket.chat/models                 | Patch |
| @rocket.chat/ui-contexts            | Major |
| @rocket.chat/account-service        | Patch |
| @rocket.chat/authorization-service  | Patch |
| @rocket.chat/ddp-streamer           | Patch |
| @rocket.chat/stream-hub-service     | Patch |
| @rocket.chat/omnichannel-services   | Patch |
| @rocket.chat/presence               | Patch |
| rocketchat-services                 | Patch |
| @rocket.chat/omnichannel-transcript | Patch |
| @rocket.chat/presence-service       | Patch |
| @rocket.chat/queue-worker           | Patch |
| @rocket.chat/network-broker         | Patch |
| @rocket.chat/livechat               | Patch |
| @rocket.chat/cron                   | Patch |
| @rocket.chat/instance-status        | Patch |
| @rocket.chat/uikit-playground       | Patch |
| @rocket.chat/fuselage-ui-kit        | Major |
| @rocket.chat/gazzodown              | Major |
| @rocket.chat/ui-avatar              | Major |
| @rocket.chat/ui-client              | Major |
| @rocket.chat/ui-video-conf          | Major |
| @rocket.chat/ui-voip                | Major |
| @rocket.chat/web-ui-registration    | Major |
| @rocket.chat/core-typings           | Minor |
| @rocket.chat/apps                   | Patch |
| @rocket.chat/freeswitch             | Patch |
| @rocket.chat/model-typings          | Patch |
| @rocket.chat/license                | Patch |
| @rocket.chat/pdf-worker             | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/RocketChat/Rocket.Chat/new/feat/openapi?filename=.changeset/khaki-ways-vanish.md&value=---%0A%22%40rocket.chat%2Fmeteor%22%3A%20patch%0A%22%40rocket.chat%2Frest-typings%22%3A%20patch%0A---%0A%0Afeat%3A%20add%20OpenAPI%20support%0A)


author_association: NONE
user: login: codecov[bot]
created_at: 2025-01-21T03:20:36Z
body: ## [Codecov](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/34983?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 59.65%. Comparing base [(`77db2bc`)](https://app.codecov.io/gh/RocketChat/Rocket.Chat/commit/77db2bca997455594295a492cda1689d49376e31?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat) to head [(`64f803b`)](https://app.codecov.io/gh/RocketChat/Rocket.Chat/commit/64f803bdb68e2c524234782b14cf5332f3ebf368?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat).
> Report is 15 commits behind head on develop.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/34983/graphs/tree.svg?width=650&height=150&src=pr&token=lEAH159Fca&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat)](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/34983?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat)

```diff
@@           Coverage Diff            @@
##           develop   #34983   +/-   ##
========================================
  Coverage    59.65%   59.65%           
========================================
  Files         2828     2828           
  Lines        68528    68528           
  Branches     15188    15188           
========================================
  Hits         40879    40879           
  Misses       25001    25001           
  Partials      2648     2648           
```

| [Flag](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/34983/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat) | Coverage Δ | |
|---|---|---|
| [unit](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/34983/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat) | `75.87% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>
<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-01-21T03:20:39Z
body: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.6.0
:---:
| <p></p> :rocket: View preview at <br> https://RocketChat.github.io/Rocket.Chat/pr-preview/pr-34983/ <br><br>
| <h6>Built to branch [`gh-pages`](https://github.com/RocketChat/Rocket.Chat/tree/gh-pages) at 2025-03-13 19:02 UTC. <br> Preview will be ready when the [GitHub Pages deployment](https://github.com/RocketChat/Rocket.Chat/deployments) is complete. <br><br> </h6>
<!-- Sticky Pull Request Commentpr-preview -->
author_association: CONTRIBUTOR
user: login: ggazzo
created_at: 2025-02-28T20:11:38Z
body: @kody start-review
author_association: MEMBER
user: login: kody-ai[bot]
created_at: 2025-02-28T20:11:43Z
body: ## Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.



<details>
<summary>Kody Guide: Usage and Configuration</summary>

<details>
<summary>Interacting with Kody</summary>

- **Request a Review:** Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

- **Provide Feedback:** Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

</details>

<details>
<summary>Current Kody Configuration</summary>

<details>
<summary>Review Options</summary>

The following review options are enabled or disabled:

| Options                        | Enabled |
|-------------------------------|---------|
| **Security** | ✅ |
| **Code Style** | ✅ |
| **Kody Rules** | ✅ |
| **Refactoring** | ✅ |
| **Error Handling** | ✅ |
| **Maintainability** | ✅ |
| **Potential Issues** | ✅ |
| **Documentation And Comments** | ✅ |
| **Performance And Optimization** | ✅ |

</details>

**[Access your configuration settings here.](https://app.kodus.io/automations/AutomationCodeReview/global/general)**

</details>
</details>

<!-- kody-codereview -->
&#8203;
author_association: NONE
user: login: ggazzo
created_at: 2025-03-12T21:51:09Z
body: @kody start-review
author_association: MEMBER
user: login: kody-ai[bot]
created_at: 2025-03-12T21:51:29Z
body: ## Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.



<details>
<summary>Kody Guide: Usage and Configuration</summary>

<details>
<summary>Interacting with Kody</summary>

- **Request a Review:** Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

- **Provide Feedback:** Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

</details>

<details>
<summary>Current Kody Configuration</summary>

<details>
<summary>Review Options</summary>

The following review options are enabled or disabled:

| Options                        | Enabled |
|-------------------------------|---------|
| **Security** | ✅ |
| **Code Style** | ✅ |
| **Kody Rules** | ✅ |
| **Refactoring** | ✅ |
| **Error Handling** | ✅ |
| **Maintainability** | ✅ |
| **Potential Issues** | ✅ |
| **Documentation And Comments** | ✅ |
| **Performance And Optimization** | ✅ |

</details>

**[Access your configuration settings here.](https://app.kodus.io/automations/AutomationCodeReview/global/general)**

</details>
</details>

<!-- kody-codereview -->
&#8203;
author_association: NONE
user: login: ahmed-n-abdeltwab
created_at: 2025-03-31T21:34:17Z
body: What is the current status of endpoint migration? Have all documented endpoints been converted to the new pattern, or are there still pending ones?
author_association: CONTRIBUTOR