feat: add Fastify API server with health check and env config

This commit is contained in:
Siddharth Kumar Sah
2026-03-22 02:46:34 +08:00
parent 19e48203bb
commit 1e7fb11da0
6 changed files with 1185 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@stirling-image/api",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
"@stirling-image/shared": "workspace:*",
"fastify": "^5.2.0",
"@fastify/static": "^8.1.0",
"@fastify/multipart": "^9.0.0",
"@fastify/cors": "^11.0.0",
"@fastify/rate-limit": "^10.2.0",
"@fastify/swagger": "^9.4.0",
"@fastify/swagger-ui": "^5.2.0",
"dotenv": "^16.4.0",
"zod": "^3.24.0"
},
"devDependencies": {
"typescript": "^5.7.0",
"tsx": "^4.19.0",
"@types/node": "^22.0.0"
}
}
+4
View File
@@ -0,0 +1,4 @@
import "dotenv/config";
import { loadEnv } from "./lib/env.js";
export const env = loadEnv();
+36
View File
@@ -0,0 +1,36 @@
import Fastify from "fastify";
import cors from "@fastify/cors";
import rateLimit from "@fastify/rate-limit";
import { env } from "./config.js";
import { APP_VERSION } from "@stirling-image/shared";
const app = Fastify({
logger: true,
bodyLimit: env.MAX_UPLOAD_SIZE_MB * 1024 * 1024,
});
// Plugins
await app.register(cors, { origin: true });
await app.register(rateLimit, {
max: env.RATE_LIMIT_PER_MIN,
timeWindow: "1 minute",
});
// Health check
app.get("/api/v1/health", async () => ({
status: "healthy",
version: APP_VERSION,
uptime: process.uptime().toFixed(0) + "s",
storage: { mode: env.STORAGE_MODE, available: "N/A" },
queue: { active: 0, pending: 0 },
ai: {},
}));
// Start
try {
await app.listen({ port: env.PORT, host: "0.0.0.0" });
console.log(`Stirling Image API running on port ${env.PORT}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
+30
View File
@@ -0,0 +1,30 @@
import { z } from "zod";
const envSchema = z.object({
PORT: z.coerce.number().default(1349),
AUTH_ENABLED: z
.enum(["true", "false"])
.default("true")
.transform((v) => v === "true"),
DEFAULT_USERNAME: z.string().default("admin"),
DEFAULT_PASSWORD: z.string().default("admin"),
STORAGE_MODE: z.enum(["local", "s3"]).default("local"),
FILE_MAX_AGE_HOURS: z.coerce.number().default(24),
CLEANUP_INTERVAL_MINUTES: z.coerce.number().default(30),
MAX_UPLOAD_SIZE_MB: z.coerce.number().default(100),
MAX_BATCH_SIZE: z.coerce.number().default(200),
CONCURRENT_JOBS: z.coerce.number().default(3),
MAX_MEGAPIXELS: z.coerce.number().default(100),
RATE_LIMIT_PER_MIN: z.coerce.number().default(100),
DB_PATH: z.string().default("./data/stirling.db"),
WORKSPACE_PATH: z.string().default("./tmp/workspace"),
DEFAULT_THEME: z.enum(["light", "dark"]).default("light"),
DEFAULT_LOCALE: z.string().default("en"),
APP_NAME: z.string().default("Stirling Image"),
});
export type Env = z.infer<typeof envSchema>;
export function loadEnv(): Env {
return envSchema.parse(process.env);
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"lib": ["ES2022"],
"module": "ESNext",
"target": "ES2022"
},
"include": ["src/**/*"]
}