diff --git a/apps/journal/app/routes/api.v1.activities._index.ts b/apps/journal/app/routes/api.v1.activities._index.ts index d9545b1..a323e88 100644 --- a/apps/journal/app/routes/api.v1.activities._index.ts +++ b/apps/journal/app/routes/api.v1.activities._index.ts @@ -5,6 +5,7 @@ import { PaginationQuerySchema, CreateActivityRequestSchema, ERROR_CODES, + zodIssuesToFieldErrors, } from "@trails-cool/api"; /** GET /api/v1/activities — paginated activity list */ @@ -59,7 +60,7 @@ export async function action({ request }: Route.ActionArgs) { const parsed = CreateActivityRequestSchema.safeParse(body); if (!parsed.success) { return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed", - parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))); + zodIssuesToFieldErrors(parsed.error)); } const id = await createActivity(user.id, { diff --git a/apps/journal/app/routes/api.v1.routes.$id.ts b/apps/journal/app/routes/api.v1.routes.$id.ts index 8458312..86e3690 100644 --- a/apps/journal/app/routes/api.v1.routes.$id.ts +++ b/apps/journal/app/routes/api.v1.routes.$id.ts @@ -1,7 +1,7 @@ import type { Route } from "./+types/api.v1.routes.$id"; import { requireApiUser, apiError } from "~/lib/api-guard.server"; import { getRouteWithVersions, updateRoute, deleteRoute } from "~/lib/routes.server"; -import { UpdateRouteRequestSchema, ERROR_CODES } from "@trails-cool/api"; +import { UpdateRouteRequestSchema, ERROR_CODES, zodIssuesToFieldErrors } from "@trails-cool/api"; /** GET /api/v1/routes/:id — full route detail */ export async function loader({ request, params }: Route.LoaderArgs) { @@ -45,7 +45,7 @@ export async function action({ request, params }: Route.ActionArgs) { const parsed = UpdateRouteRequestSchema.safeParse(body); if (!parsed.success) { return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed", - parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))); + zodIssuesToFieldErrors(parsed.error)); } await updateRoute(params.id, user.id, parsed.data); diff --git a/apps/journal/app/routes/api.v1.routes._index.ts b/apps/journal/app/routes/api.v1.routes._index.ts index 8ce9821..5f72767 100644 --- a/apps/journal/app/routes/api.v1.routes._index.ts +++ b/apps/journal/app/routes/api.v1.routes._index.ts @@ -5,6 +5,7 @@ import { PaginationQuerySchema, CreateRouteRequestSchema, ERROR_CODES, + zodIssuesToFieldErrors, } from "@trails-cool/api"; /** GET /api/v1/routes — paginated route list */ @@ -58,7 +59,7 @@ export async function action({ request }: Route.ActionArgs) { const parsed = CreateRouteRequestSchema.safeParse(body); if (!parsed.success) { return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed", - parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))); + zodIssuesToFieldErrors(parsed.error)); } const id = await createRoute(user.id, parsed.data); diff --git a/apps/journal/app/routes/api.v1.routes.compute.ts b/apps/journal/app/routes/api.v1.routes.compute.ts index 077d733..cd111ff 100644 --- a/apps/journal/app/routes/api.v1.routes.compute.ts +++ b/apps/journal/app/routes/api.v1.routes.compute.ts @@ -1,6 +1,6 @@ import type { Route } from "./+types/api.v1.routes.compute"; import { requireApiUser, apiError } from "~/lib/api-guard.server"; -import { ComputeRouteRequestSchema, ERROR_CODES } from "@trails-cool/api"; +import { ComputeRouteRequestSchema, ERROR_CODES, zodIssuesToFieldErrors } from "@trails-cool/api"; const PLANNER_URL = process.env.PLANNER_URL ?? "http://localhost:3001"; @@ -13,7 +13,7 @@ export async function action({ request }: Route.ActionArgs) { const parsed = ComputeRouteRequestSchema.safeParse(body); if (!parsed.success) { return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed", - parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))); + zodIssuesToFieldErrors(parsed.error)); } try { diff --git a/apps/journal/app/routes/api.v1.uploads.ts b/apps/journal/app/routes/api.v1.uploads.ts index 83a18da..4998bdc 100644 --- a/apps/journal/app/routes/api.v1.uploads.ts +++ b/apps/journal/app/routes/api.v1.uploads.ts @@ -1,6 +1,6 @@ import type { Route } from "./+types/api.v1.uploads"; import { requireApiUser, apiError } from "~/lib/api-guard.server"; -import { PresignedUploadRequestSchema, ERROR_CODES } from "@trails-cool/api"; +import { PresignedUploadRequestSchema, ERROR_CODES, zodIssuesToFieldErrors } from "@trails-cool/api"; import { randomUUID } from "node:crypto"; const S3_ENDPOINT = process.env.S3_ENDPOINT ?? "http://localhost:3902"; @@ -16,7 +16,7 @@ export async function action({ request }: Route.ActionArgs) { const parsed = PresignedUploadRequestSchema.safeParse(body); if (!parsed.success) { return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed", - parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))); + zodIssuesToFieldErrors(parsed.error)); } const { filename, resourceType, resourceId } = parsed.data; diff --git a/packages/api/src/errors.ts b/packages/api/src/errors.ts index 7d7fb88..381dd95 100644 --- a/packages/api/src/errors.ts +++ b/packages/api/src/errors.ts @@ -14,6 +14,18 @@ export const ApiErrorResponseSchema = z.object({ export type FieldError = z.infer; export type ApiErrorResponse = z.infer; +/** + * Map a ZodError's issues to the FieldError[] shape used in API + * validation responses. Keeps the path-flattening rule in one place so + * every route returns validation errors in the same shape. + */ +export function zodIssuesToFieldErrors(error: z.ZodError): FieldError[] { + return error.issues.map((i) => ({ + field: i.path.join("."), + message: i.message, + })); +} + /** Standard error codes */ export const ERROR_CODES = { VALIDATION_ERROR: "VALIDATION_ERROR", diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 8f7e100..a6e14a6 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -4,6 +4,7 @@ export { ENDPOINTS } from "./endpoints.ts"; // Error schemas export { ApiErrorResponseSchema, FieldErrorSchema, ERROR_CODES, + zodIssuesToFieldErrors, type ApiErrorResponse, type FieldError, } from "./errors.ts";