refactor(api): version routes and externalize client base url

- serve REST endpoints under /api/v1 with root exceptions for health and mcp

- move swagger UI to /docs and allow direct frontend calls via CORS

- remove Vite proxy/hash routing and rely on env-driven VITE_API_URL
This commit is contained in:
2026-04-10 22:53:32 +03:00
parent ccce3381c1
commit 11db983ea6
7 changed files with 38 additions and 19 deletions
+3
View File
@@ -0,0 +1,3 @@
# Frontend API base URL.
# Example for local Docker server: backend container is exposed on localhost:13000.
VITE_API_URL=http://localhost:13000/api/v1
+3 -3
View File
@@ -13,9 +13,9 @@ import type {
} from './types'; } from './types';
import { emitApiErrorToast } from '../lib/toast-events'; import { emitApiErrorToast } from '../lib/toast-events';
// In dev, Vite proxies /environments /sessions /scenarios to localhost:3000. // API calls are versioned under /api/v1 by default.
// In production (or when VITE_API_URL is set) we hit the configured origin directly. // Set VITE_API_URL (for example, http://localhost:13000/api/v1) to use a different origin.
const BASE_URL: string = (import.meta.env.VITE_API_URL as string | undefined) ?? ''; const BASE_URL = ((import.meta.env.VITE_API_URL as string | undefined) ?? '/api/v1').replace(/\/$/, '');
async function request<T>(path: string, init?: RequestInit): Promise<T> { async function request<T>(path: string, init?: RequestInit): Promise<T> {
let res: Response; let res: Response;
+3 -3
View File
@@ -1,6 +1,6 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import './i18n'; import './i18n';
import './ui/tokens.css'; import './ui/tokens.css';
import App from './App'; import App from './App';
@@ -9,9 +9,9 @@ import { ToastProvider } from './ui';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<ToastProvider> <ToastProvider>
<HashRouter> <BrowserRouter>
<App /> <App />
</HashRouter> </BrowserRouter>
</ToastProvider> </ToastProvider>
</StrictMode>, </StrictMode>,
); );
-9
View File
@@ -68,15 +68,6 @@ export default defineConfig({
}, },
}, },
}, },
server: {
proxy: {
'/environments': 'http://localhost:13000',
'/credentials': 'http://localhost:13000',
'/snippets': 'http://localhost:13000',
'/sessions': 'http://localhost:13000',
'/scenarios': 'http://localhost:13000',
},
},
test: { test: {
projects: [{ projects: [{
extends: true, extends: true,
+1 -1
View File
@@ -23,7 +23,7 @@ export class AppConfig {
KEYS_DIR: string = "keys"; KEYS_DIR: string = "keys";
@IsString() @IsString()
DB_PATH: string = "data/sessions.db"; DB_PATH: string = "data/liqa.db";
@IsInt() @IsInt()
@Min(1) @Min(1)
+7
View File
@@ -4,6 +4,13 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
@ApiTags("health") @ApiTags("health")
@Controller() @Controller()
export class HealthController { export class HealthController {
@Get("health")
@ApiOperation({ summary: "Health check" })
@ApiResponse({ status: 200, description: "Service is healthy" })
health(): { status: string } {
return { status: "ok" };
}
@Get("healthz") @Get("healthz")
@ApiOperation({ summary: "Health check" }) @ApiOperation({ summary: "Health check" })
@ApiResponse({ status: 200, description: "Service is healthy" }) @ApiResponse({ status: 200, description: "Service is healthy" })
+21 -3
View File
@@ -1,7 +1,7 @@
import { NestFactory } from "@nestjs/core"; import { NestFactory } from "@nestjs/core";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
import { AppConfig } from "./config/app.config"; import { AppConfig } from "./config/app.config";
import { ValidationPipe } from "@nestjs/common"; import { RequestMethod, ValidationPipe } from "@nestjs/common";
import { TraceLogger } from "./common/trace-logger"; import { TraceLogger } from "./common/trace-logger";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { AppModule } from "./app.module"; import { AppModule } from "./app.module";
@@ -11,11 +11,20 @@ import { LoggingInterceptor } from "./interceptors/logging.interceptor";
import { name as pkgName, version as pkgVersion } from "../package.json"; import { name as pkgName, version as pkgVersion } from "../package.json";
async function bootstrap() { async function bootstrap() {
const API_PREFIX = "api/v1";
const logger = new TraceLogger("Bootstrap"); const logger = new TraceLogger("Bootstrap");
const app = await NestFactory.create(AppModule, { const app = await NestFactory.create(AppModule, {
logger: new TraceLogger("Bootstrap", { timestamp: true }), logger: new TraceLogger("Bootstrap", { timestamp: true }),
}); });
app.setGlobalPrefix(API_PREFIX, {
exclude: [
{ path: "health", method: RequestMethod.ALL },
{ path: "healthz", method: RequestMethod.ALL },
{ path: "mcp", method: RequestMethod.ALL },
],
});
app.useGlobalPipes(new ValidationPipe({ transform: true })); app.useGlobalPipes(new ValidationPipe({ transform: true }));
app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new TraceInterceptor(), new LoggingInterceptor()); app.useGlobalInterceptors(new TraceInterceptor(), new LoggingInterceptor());
@@ -24,6 +33,15 @@ async function bootstrap() {
const port = config.get("PORT"); const port = config.get("PORT");
const nodeEnv = config.get("NODE_ENV"); const nodeEnv = config.get("NODE_ENV");
app.enableCors({
origin: [
"http://localhost:5173",
"http://127.0.0.1:5173",
],
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"],
credentials: true,
});
const swaggerConfig = new DocumentBuilder() const swaggerConfig = new DocumentBuilder()
.setTitle(pkgName) .setTitle(pkgName)
.setDescription(`${pkgName} API`) .setDescription(`${pkgName} API`)
@@ -31,14 +49,14 @@ async function bootstrap() {
.build(); .build();
const document = SwaggerModule.createDocument(app, swaggerConfig); const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup("api", app, document); SwaggerModule.setup("docs", app, document);
await app.listen(port); await app.listen(port);
logger.log( logger.log(
`Application "${pkgName}" v${pkgVersion} running on port ${port} [${nodeEnv}]`, `Application "${pkgName}" v${pkgVersion} running on port ${port} [${nodeEnv}]`,
); );
logger.log(`Swagger UI available at http://localhost:${port}/api`); logger.log(`Swagger UI available at http://localhost:${port}/docs`);
logger.log(`MCP endpoint available at http://localhost:${port}/mcp`); logger.log(`MCP endpoint available at http://localhost:${port}/mcp`);
} }