Files
MiruroAPI/server.js
T
Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ 4b083a9bf7 docs: rewrite all files with AniKotoAPI documentation style
- Box-style file headers (======= border, @description, @exports, @author, @license)
- Section headers (═════════════════════════════════════════)
- Function-level JSDoc comments with @param, @returns, @example
- Inline notes for non-obvious logic (lazy eviction, pipe decoding, slug resolution)
- Feature markers (---- FEATURE: description ----)
- Footer on every module (═══════════ END: filename ═══════════)
- Added /stats and /openapi endpoints to router
2026-06-09 14:27:29 +05:30

204 lines
8.5 KiB
JavaScript

/*
* ======= • ======= • ======= • ======= • =======• =======
* MiruroAPI — server.js
* Repository: https://github.com/Shineii86/MiruroAPI
*
* @description
* Main entry point for the MiruroAPI Express server.
* Configures CORS, middleware, static files, Swagger docs,
* API routes, and 404 handling. Starts the server on the
* configured port.
*
* @exports
* None (side-effect: starts Express server)
*
* @author Shinei Nouzen
* @license MIT
* ======= • ======= • ======= • ======= • =======• =======
*/
const express = require("express");
const cors = require("cors");
const path = require("path");
require("dotenv").config();
// ══════════════════════════════════════════════════════════════
// SERVER CONFIGURATION
// ══════════════════════════════════════════════════════════════
const app = express();
const PORT = process.env.PORT || 3000;
const allowedOrigins = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(",")
: null;
// ══════════════════════════════════════════════════════════════
// CORS MIDDLEWARE
// ══════════════════════════════════════════════════════════════
// NOTE: Single unified CORS middleware — handles all origin validation
app.use((req, res, next) => {
const origin = req.headers.origin;
if (
!allowedOrigins ||
allowedOrigins.includes("*") ||
(origin && allowedOrigins.includes(origin))
) {
res.setHeader("Access-Control-Allow-Origin", origin || "*");
}
res.setHeader("Access-Control-Allow-Methods", "GET");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") {
return res.status(204).end();
}
next();
});
// ---- FEATURE: Security headers ----
app.use((req, res, next) => {
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-XSS-Protection", "1; mode=block");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
next();
});
// ══════════════════════════════════════════════════════════════
// STATIC FILES
// ══════════════════════════════════════════════════════════════
app.use(express.static(path.join(__dirname, "public"), { redirect: false }));
// ══════════════════════════════════════════════════════════════
// SWAGGER UI DOCS
// ══════════════════════════════════════════════════════════════
// ---- FEATURE: Interactive Swagger UI documentation at /docs ----
app.get("/docs", (req, res) => {
res.send(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MiruroAPI — Swagger UI</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
<style>body{margin:0;background:#0b1622;}</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({
url: "/openapi.json",
dom_id: "#swagger-ui",
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
layout: "BaseLayout",
deepLinking: true,
defaultModelsExpandDepth: -1,
docExpansion: "list",
});
</script>
</body>
</html>`);
});
// ══════════════════════════════════════════════════════════════
// RESPONSE HELPERS
// ══════════════════════════════════════════════════════════════
// ---- FEATURE: Standardized JSON response wrapper ----
/**
* Wraps data in a standardized success JSON response.
*
* @param {object} res - Express response object
* @param {*} data - The data to return in the response
* @param {number} status - HTTP status code (default: 200)
*/
const jsonResponse = (res, data, status = 200) =>
res.status(status).json({ success: true, results: data });
// ---- FEATURE: Standardized error response wrapper ----
/**
* Returns a standardized error JSON response.
*
* @param {object} res - Express response object
* @param {string} message - Error message to return
* @param {number} status - HTTP status code (default: 500)
*/
const jsonError = (res, message = "Internal server error", status = 500) =>
res.status(status).json({ success: false, message });
// ══════════════════════════════════════════════════════════════
// RATE LIMITING
// ══════════════════════════════════════════════════════════════
// ---- FEATURE: Rate limiting (100 requests per minute per IP) ----
const requestCounts = new Map();
app.use((req, res, next) => {
const ip = req.ip || req.connection.remoteAddress;
const now = Date.now();
const windowMs = 60000;
const maxRequests = 100;
if (!requestCounts.has(ip)) {
requestCounts.set(ip, []);
}
const timestamps = requestCounts.get(ip).filter((t) => now - t < windowMs);
requestCounts.set(ip, timestamps);
if (timestamps.length >= maxRequests) {
return res.status(429).json({
success: false,
message: "Rate limit exceeded. Try again later.",
});
}
timestamps.push(now);
next();
});
// ══════════════════════════════════════════════════════════════
// API ROUTES
// ══════════════════════════════════════════════════════════════
const apiRoutes = require("./src/routes/apiRoutes");
app.use("/api", apiRoutes(jsonResponse, jsonError));
// ══════════════════════════════════════════════════════════════
// 404 HANDLER
// ══════════════════════════════════════════════════════════════
// ---- FEATURE: Global error handler ----
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
success: false,
message: err.message || "Internal server error",
});
});
// ---- FEATURE: Catch-all 404 handler for undefined routes ----
app.use((req, res) => {
if (req.path.startsWith("/api/")) {
return res.status(404).json({
success: false,
message: "Endpoint not found",
});
}
res.sendFile(path.join(__dirname, "public", "index.html"));
});
// ══════════════════════════════════════════════════════════════
// SERVER START
// ══════════════════════════════════════════════════════════════
app.listen(PORT, () => {
console.info(`MiruroAPI listening at ${PORT}`);
});
module.exports = app;
// ══════════════════════════════════════════════════════════════ END: server.js