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:
@@ -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
|
||||||
@@ -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
@@ -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>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user