diff --git a/config/api-url-contract.ts b/config/api-url-contract.ts new file mode 100644 index 00000000..85f44af6 --- /dev/null +++ b/config/api-url-contract.ts @@ -0,0 +1,21 @@ +/** Absolute VITE_API_URL values must end with this segment (API_PATHS omit /api). */ +export const ABSOLUTE_API_URL_SUFFIX = /\/api\/?$/; + +/** + * Fails fast when an absolute VITE_API_URL omits the /api segment. + * Relative values (e.g. /api) are valid and skipped. + */ +export function assertAbsoluteApiUrlHasApiSegment(apiUrl: string | undefined): void { + const trimmed = apiUrl?.trim(); + if (!trimmed?.startsWith("http")) { + return; + } + + if (!ABSOLUTE_API_URL_SUFFIX.test(trimmed)) { + throw new Error( + `VITE_API_URL absolute URLs must end with /api (got: ${trimmed}). ` + + "API route paths do not include /api — set VITE_API_URL to the full API base, e.g. https://api.example.com/api. " + + "See README.md and .env.example.", + ); + } +} diff --git a/tsconfig.node.json b/tsconfig.node.json index a02b1983..3e43f464 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -16,5 +16,10 @@ "outDir": "./node_modules/.tmp/tsconfig.node", "types": ["node"] }, - "include": ["vite.config.ts", "vitest.config.ts"] + "include": [ + "vite.config.ts", + "vitest.config.ts", + "config/api-url-contract.ts", + "config/api-url-contract.test.ts" + ] } diff --git a/vite.config.ts b/vite.config.ts index c25e540b..204b1ce1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,10 +2,13 @@ import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import path from "node:path"; import { defineConfig, loadEnv, transformWithEsbuild } from "vite"; +import { assertAbsoluteApiUrlHasApiSegment } from "./config/api-url-contract"; export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); + assertAbsoluteApiUrlHasApiSegment(env.VITE_API_URL); + return { plugins: [ {