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';
|
||||
import { emitApiErrorToast } from '../lib/toast-events';
|
||||
|
||||
// In dev, Vite proxies /environments /sessions /scenarios to localhost:3000.
|
||||
// In production (or when VITE_API_URL is set) we hit the configured origin directly.
|
||||
const BASE_URL: string = (import.meta.env.VITE_API_URL as string | undefined) ?? '';
|
||||
// API calls are versioned under /api/v1 by default.
|
||||
// Set VITE_API_URL (for example, http://localhost:13000/api/v1) to use a different origin.
|
||||
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> {
|
||||
let res: Response;
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './i18n';
|
||||
import './ui/tokens.css';
|
||||
import App from './App';
|
||||
@@ -9,9 +9,9 @@ import { ToastProvider } from './ui';
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<ToastProvider>
|
||||
<HashRouter>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</BrowserRouter>
|
||||
</ToastProvider>
|
||||
</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: {
|
||||
projects: [{
|
||||
extends: true,
|
||||
|
||||
@@ -23,7 +23,7 @@ export class AppConfig {
|
||||
KEYS_DIR: string = "keys";
|
||||
|
||||
@IsString()
|
||||
DB_PATH: string = "data/sessions.db";
|
||||
DB_PATH: string = "data/liqa.db";
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
|
||||
@@ -4,6 +4,13 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
@ApiTags("health")
|
||||
@Controller()
|
||||
export class HealthController {
|
||||
@Get("health")
|
||||
@ApiOperation({ summary: "Health check" })
|
||||
@ApiResponse({ status: 200, description: "Service is healthy" })
|
||||
health(): { status: string } {
|
||||
return { status: "ok" };
|
||||
}
|
||||
|
||||
@Get("healthz")
|
||||
@ApiOperation({ summary: "Health check" })
|
||||
@ApiResponse({ status: 200, description: "Service is healthy" })
|
||||
|
||||
+21
-3
@@ -1,7 +1,7 @@
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { ConfigService } from "@nestjs/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 { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||
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";
|
||||
|
||||
async function bootstrap() {
|
||||
const API_PREFIX = "api/v1";
|
||||
const logger = new TraceLogger("Bootstrap");
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
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.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.useGlobalInterceptors(new TraceInterceptor(), new LoggingInterceptor());
|
||||
@@ -24,6 +33,15 @@ async function bootstrap() {
|
||||
const port = config.get("PORT");
|
||||
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()
|
||||
.setTitle(pkgName)
|
||||
.setDescription(`${pkgName} API`)
|
||||
@@ -31,14 +49,14 @@ async function bootstrap() {
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||
SwaggerModule.setup("api", app, document);
|
||||
SwaggerModule.setup("docs", app, document);
|
||||
|
||||
await app.listen(port);
|
||||
|
||||
logger.log(
|
||||
`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`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user