mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add Fastify API server with health check and env config
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import "dotenv/config";
|
||||
import { loadEnv } from "./lib/env.js";
|
||||
|
||||
export const env = loadEnv();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"lib": ["ES2022"],
|
||||
"module": "ESNext",
|
||||
"target": "ES2022"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user