diff --git a/src/constants/public-endpoint-cache.constants.ts b/src/constants/public-endpoint-cache.constants.ts new file mode 100644 index 0000000..b7fd969 --- /dev/null +++ b/src/constants/public-endpoint-cache.constants.ts @@ -0,0 +1,26 @@ +/** + * Public endpoint cache settings. + * + * Keep this file limited to lightweight, reusable constants (no runtime logic). + */ +export const PUBLIC_ENDPOINT_CACHE_SECONDS = { + short: 300, + medium: 3600, + long: 86400, +} as const; + +export const PUBLIC_ENDPOINT_CACHE_PRESETS = { + short: { + maxAge: PUBLIC_ENDPOINT_CACHE_SECONDS.short, + type: 'public' as const, + }, + medium: { + maxAge: PUBLIC_ENDPOINT_CACHE_SECONDS.medium, + type: 'public' as const, + }, + long: { + maxAge: PUBLIC_ENDPOINT_CACHE_SECONDS.long, + type: 'public' as const, + }, +} as const; + diff --git a/src/middlewares/cache-control.middleware.ts b/src/middlewares/cache-control.middleware.ts index f4a63a7..350ac57 100644 --- a/src/middlewares/cache-control.middleware.ts +++ b/src/middlewares/cache-control.middleware.ts @@ -1,5 +1,6 @@ // src/middlewares/cache-control.middleware.ts import { Request, Response, NextFunction } from 'express'; +import { PUBLIC_ENDPOINT_CACHE_PRESETS } from '../constants/public-endpoint-cache.constants'; /** * Cache control options for different types of endpoints. @@ -90,17 +91,17 @@ export const CachePresets = { /** * Short cache for frequently updated public data (5 minutes) */ - publicShort: { maxAge: 300, type: 'public' as const }, + publicShort: PUBLIC_ENDPOINT_CACHE_PRESETS.short, /** * Medium cache for moderately stable public data (1 hour) */ - publicMedium: { maxAge: 3600, type: 'public' as const }, + publicMedium: PUBLIC_ENDPOINT_CACHE_PRESETS.medium, /** * Long cache for stable public data (24 hours) */ - publicLong: { maxAge: 86400, type: 'public' as const }, + publicLong: PUBLIC_ENDPOINT_CACHE_PRESETS.long, /** * Private cache for user-specific data (5 minutes) diff --git a/src/modules/creator/creator.controller.ts b/src/modules/creator/creator.controller.ts index 0c61542..2f5d4f5 100644 --- a/src/modules/creator/creator.controller.ts +++ b/src/modules/creator/creator.controller.ts @@ -1,6 +1,5 @@ // src/modules/creator/creator.controller.ts import { Request, Response } from 'express'; -import { ZodError } from 'zod'; import { z } from 'zod'; import { sendPaginatedSuccess, @@ -11,6 +10,7 @@ import { import { getPaginatedCreators } from './creator.service'; import { parseCreatorSortOptions } from './creator.utils'; import { safeIntParam } from '../../utils/query.utils'; +import { parsePublicQuery } from '../../utils/public-query-parse.utils'; import { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, @@ -37,7 +37,11 @@ const LegacyCreatorQuerySchema = z.object({ export async function listCreators(req: Request, res: Response) { try { - const { page, limit, sortBy, sortOrder } = LegacyCreatorQuerySchema.parse(req.query); + const parsed = parsePublicQuery(LegacyCreatorQuerySchema, req.query); + if (!parsed.ok) { + return sendValidationError(res, 'Invalid query parameters', parsed.details); + } + const { page, limit, sortBy, sortOrder } = parsed.data; const sort = parseCreatorSortOptions(sortBy, sortOrder); @@ -55,13 +59,6 @@ export async function listCreators(req: Request, res: Response) { 'Creators retrieved successfully' ); } catch (error) { - if (error instanceof ZodError) { - const details = error.errors.map(err => ({ - field: err.path.join('.'), - message: err.message, - })); - return sendValidationError(res, 'Invalid query parameters', details); - } console.error('Error listing creators:', error); return sendError( res, diff --git a/src/modules/creators/creators.controllers.ts b/src/modules/creators/creators.controllers.ts index c32e363..5c28101 100644 --- a/src/modules/creators/creators.controllers.ts +++ b/src/modules/creators/creators.controllers.ts @@ -10,7 +10,7 @@ import { sendSuccess, sendValidationError, } from '../../utils/api-response.utils'; -import { ZodError } from 'zod'; +import { parsePublicQuery } from '../../utils/public-query-parse.utils'; /** * Controller for GET /api/v1/creators @@ -21,7 +21,11 @@ import { ZodError } from 'zod'; export const httpListCreators: AsyncController = async (req, res, next) => { try { // Validate query parameters - const validatedQuery = CreatorListQuerySchema.parse(req.query); + const parsed = parsePublicQuery(CreatorListQuerySchema, req.query); + if (!parsed.ok) { + return sendValidationError(res, 'Invalid query parameters', parsed.details); + } + const validatedQuery = parsed.data; // Fetch creators and total count const [creators, total] = await fetchCreatorList(validatedQuery); @@ -39,13 +43,6 @@ export const httpListCreators: AsyncController = async (req, res, next) => { sendSuccess(res, response); } catch (error) { - if (error instanceof ZodError) { - const details = error.errors.map(err => ({ - field: err.path.join('.'), - message: err.message, - })); - return sendValidationError(res, 'Invalid query parameters', details); - } next(error); } }; diff --git a/src/modules/creators/creators.routes.ts b/src/modules/creators/creators.routes.ts index 5be776d..aa248e7 100644 --- a/src/modules/creators/creators.routes.ts +++ b/src/modules/creators/creators.routes.ts @@ -1,9 +1,7 @@ import { Router } from 'express'; import { httpListCreators } from './creators.controllers'; -import { - cacheControl, - CachePresets, -} from '../../middlewares/cache-control.middleware'; +import { cacheControl } from '../../middlewares/cache-control.middleware'; +import { PUBLIC_ENDPOINT_CACHE_PRESETS } from '../../constants/public-endpoint-cache.constants'; const creatorsRouter = Router(); @@ -15,7 +13,7 @@ const creatorsRouter = Router(); */ creatorsRouter.get( '/', - cacheControl(CachePresets.publicShort), + cacheControl(PUBLIC_ENDPOINT_CACHE_PRESETS.short), httpListCreators ); diff --git a/src/utils/public-query-parse.utils.ts b/src/utils/public-query-parse.utils.ts new file mode 100644 index 0000000..164689c --- /dev/null +++ b/src/utils/public-query-parse.utils.ts @@ -0,0 +1,36 @@ +import { z, ZodError, ZodTypeAny } from 'zod'; + +export type PublicQueryValidationDetail = { + field: string; + message: string; +}; + +export type PublicQueryParseResult = + | { ok: true; data: T } + | { ok: false; details: PublicQueryValidationDetail[] }; + +/** + * Parse and validate public endpoint query params with a predictable output shape. + * + * This helper is intentionally small and focused: + * - maps `ZodError` into `{ field, message }[]` for API validation responses + * - does not add runtime behavior beyond schema parsing and error shaping + */ +export function parsePublicQuery( + schema: S, + rawQuery: unknown +): PublicQueryParseResult> { + try { + return { ok: true, data: schema.parse(rawQuery) }; + } catch (error) { + if (error instanceof ZodError) { + const details: PublicQueryValidationDetail[] = error.errors.map(err => ({ + field: err.path.join('.'), + message: err.message, + })); + return { ok: false, details }; + } + throw error; + } +} +