Skip to content

Commit bec82ae

Browse files
SOIVclaude
andcommitted
feat(api): Phase 1.9.1 API 서버 부트스트랩
- Express 5 서버 세팅 (app.ts 팩토리 분리) - Zod 기반 환경변수 스키마 검증 (config/env.ts) — 누락 시 즉시 종료 - CORS 미들웨어 (개발 환경만 활성화) - GET /health 엔드포인트 - 글로벌 에러 핸들러 (dev 환경에서 message 노출) - esModuleInterop 활성화 (Express/cors default import 지원) - Phase 1.9.2에서 모듈 라우트 마운트 예정 TODO 주석 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 48c6914 commit bec82ae

7 files changed

Lines changed: 101 additions & 23 deletions

File tree

apps/api/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@
1313
"lint": "eslint src --ext .ts"
1414
},
1515
"devDependencies": {
16+
"@types/cors": "^2.8.17",
17+
"@types/express": "^5.0.3",
1618
"tsx": "^4.21.0",
1719
"vitest": "^2.1.9"
1820
},
1921
"dependencies": {
20-
"dotenv": "^17.3.1"
22+
"cors": "^2.8.5",
23+
"dotenv": "^17.3.1",
24+
"express": "^5.1.0",
25+
"zod": "^3.25.28"
2126
}
2227
}

apps/api/src/app.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import express from 'express';
2+
import cors from 'cors';
3+
4+
import { healthRouter } from './routes/health';
5+
import { errorHandler } from './middleware/error';
6+
7+
export function createApp() {
8+
const app = express();
9+
10+
// ── Middleware ────────────────────────────────────────────────
11+
// 개발: 모든 origin 허용 / 프로덕션: 단일 포트 통합 서빙이므로 CORS 불필요
12+
if (process.env['NODE_ENV'] !== 'production') {
13+
app.use(cors());
14+
}
15+
16+
app.use(express.json());
17+
app.use(express.urlencoded({ extended: true }));
18+
19+
// ── Core routes ───────────────────────────────────────────────
20+
app.use('/health', healthRouter);
21+
22+
// TODO(Phase 1.9.2): DB 초기화 후 모듈 라우트 마운트
23+
// const registrations = buildBackendRouteRegistrations(manifests);
24+
// registrations.forEach(({ apiBasePath, router }) => app.use(apiBasePath, router));
25+
26+
// ── Error handler (반드시 마지막) ─────────────────────────────
27+
app.use(errorHandler);
28+
29+
return app;
30+
}

apps/api/src/config/env.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { z } from 'zod';
2+
3+
const EnvSchema = z.object({
4+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
5+
PORT: z.coerce.number().int().positive().default(3000),
6+
INSTALL_MODE: z.enum(['normal', 'bypass']).optional(),
7+
});
8+
9+
export type Env = z.infer<typeof EnvSchema>;
10+
11+
export function validateEnv(raw: NodeJS.ProcessEnv): Env {
12+
const result = EnvSchema.safeParse(raw);
13+
14+
if (!result.success) {
15+
console.error('[fieldstack][api] Invalid environment variables:');
16+
for (const [field, issue] of Object.entries(result.error.flatten().fieldErrors)) {
17+
console.error(` ${field}: ${(issue as string[]).join(', ')}`);
18+
}
19+
process.exit(1);
20+
}
21+
22+
return result.data;
23+
}

apps/api/src/index.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
1-
import "dotenv/config";
1+
import 'dotenv/config';
22

3-
type InstallMode = "normal" | "bypass";
3+
import { validateEnv } from './config/env';
4+
import { createApp } from './app';
45

5-
const BOOTSTRAP_MESSAGE = "Fieldstack API bootstrap initialized";
6+
// ── 환경변수 검증 (누락·오류 시 즉시 종료) ────────────────────
7+
const env = validateEnv(process.env);
68

7-
function resolveInstallMode(env: NodeJS.ProcessEnv): InstallMode {
8-
const requestedMode = env.INSTALL_MODE;
9-
const isDevelopment = env.NODE_ENV === "development";
9+
// ── Install mode ──────────────────────────────────────────────
10+
const BOOTSTRAP_MESSAGE = 'Fieldstack API bootstrap initialized';
1011

11-
if (requestedMode === "bypass") {
12-
if (isDevelopment) {
13-
return "bypass";
14-
}
15-
16-
console.warn("[fieldstack][api] INSTALL_MODE=bypass ignored outside development");
17-
}
12+
console.log(BOOTSTRAP_MESSAGE);
13+
console.log(`[fieldstack][api] env: ${env.NODE_ENV}`);
14+
console.log(`[fieldstack][api] install mode: ${env.INSTALL_MODE ?? 'normal'}`);
1815

19-
return "normal";
16+
if (env.INSTALL_MODE === 'bypass') {
17+
console.warn('[fieldstack][api] DEV INSTALL BYPASS ACTIVE');
2018
}
2119

22-
const installMode = resolveInstallMode(process.env);
20+
// ── 서버 시작 ─────────────────────────────────────────────────
21+
const app = createApp();
2322

24-
console.log(BOOTSTRAP_MESSAGE);
25-
console.log(`[fieldstack][api] install mode: ${installMode}`);
26-
27-
if (installMode === "bypass") {
28-
console.warn("[fieldstack][api] DEV INSTALL BYPASS ACTIVE");
29-
}
23+
app.listen(env.PORT, () => {
24+
console.log(`[fieldstack][api] server listening on http://localhost:${env.PORT}`);
25+
});

apps/api/src/middleware/error.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { ErrorRequestHandler } from 'express';
2+
3+
export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
4+
const isDev = process.env['NODE_ENV'] === 'development';
5+
6+
console.error('[fieldstack][api] unhandled error:', err);
7+
8+
res.status(500).json({
9+
error: 'Internal server error',
10+
...(isDev && { message: (err as Error).message }),
11+
});
12+
};

apps/api/src/routes/health.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Router } from 'express';
2+
3+
export const healthRouter = Router();
4+
5+
healthRouter.get('/', (_req, res) => {
6+
res.json({
7+
status: 'ok',
8+
version: process.env['npm_package_version'] ?? '0.0.0',
9+
timestamp: new Date().toISOString(),
10+
});
11+
});

apps/api/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"rootDir": "src",
66
"noEmit": false,
77
"moduleResolution": "Node",
8-
"module": "CommonJS"
8+
"module": "CommonJS",
9+
"esModuleInterop": true
910
},
1011
"include": ["src/**/*.ts"]
1112
}

0 commit comments

Comments
 (0)