feat: add health, stats, OpenAPI, security headers, tests

- /api/health — uptime, version, memory usage
- /api/stats — request count, error rate, cache info
- /api/openapi — OpenAPI 3.0.3 specification
- Security headers: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection
- Request counter middleware
- CONTRIBUTING.md — contribution guidelines
- test.js — integration test suite (18 endpoints)
This commit is contained in:
Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ
2026-06-08 22:08:38 +05:30
parent 18e4a84a66
commit e9b0fba22b
5 changed files with 300 additions and 5 deletions
+7
View File
@@ -14,6 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `docs/streaming.md` — Streaming flow guide (3-step with real data)
- `docs/examples.md` — Code examples in cURL, JavaScript, Python, Node.js (all tested)
- `docs/architecture.md` — Project structure, tech stack, design decisions
- `/api/health` — Health check endpoint (uptime, version, memory)
- `/api/stats` — Cache & API statistics (requests, errors, success rate)
- `/api/openapi` — OpenAPI 3.0.3 specification
- Security headers: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection
- Request counter middleware for statistics
- `CONTRIBUTING.md` — Contribution guidelines
- `test.js` — Integration test suite (18 endpoints)
### Fixed
- Clean URLs: `/tos` and `/privacy` instead of `/tos.html` and `/privacy.html`
+84
View File
@@ -0,0 +1,84 @@
# Contributing to AniKotoAPI
Thank you for your interest in contributing! Here's how you can help.
## Getting Started
1. Fork the repository
2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/AniKotoAPI.git`
3. Create a branch: `git checkout -b feature/amazing-feature`
4. Make your changes
5. Commit: `git commit -m 'feat: add amazing feature'`
6. Push: `git push origin feature/amazing-feature`
7. Open a Pull Request
## Development Setup
```bash
# Install dependencies
npm install
# Start development server
npm run dev
# Server runs on http://localhost:4444
```
## Project Structure
```
AniKotoAPI/
├── server.js # Express entry point
├── src/
│ ├── configs/ # URL patterns, headers, ID mappings
│ ├── controllers/ # Route handlers
│ ├── extractors/ # HTML scrapers (Cheerio)
│ ├── helper/ # Cache utilities
│ └── routes/ # Express routes
├── public/ # Static files (landing page)
└── docs/ # API documentation
```
## Adding a New Endpoint
1. Create extractor in `src/extractors/yourExtractor.js`
2. Create controller in `src/controllers/yourController.js`
3. Add route in `src/routes/apiRoutes.js`
4. Update `docs/endpoints.md` with documentation
5. Test with `curl http://localhost:4444/api/your-endpoint`
## Code Style
- Use ES6+ features (import/export, async/await)
- Add JSDoc comments for new functions
- Follow existing patterns in the codebase
- Keep extractors focused on one task
## Commit Messages
Use [Conventional Commits](https://www.conventionalcommits.org/):
- `feat:` new feature
- `fix:` bug fix
- `docs:` documentation changes
- `refactor:` code refactoring
- `test:` adding tests
- `chore:` maintenance
## Pull Request Guidelines
- Keep PRs focused on one feature or fix
- Update CHANGELOG.md with your changes
- Test your changes locally before submitting
- Follow existing code style
## Reporting Issues
- Use GitHub Issues for bug reports
- Include steps to reproduce
- Include expected vs actual behavior
- Include your Node.js version
## License
By contributing, you agree that your contributions will be licensed under the MIT License.
+10
View File
@@ -67,6 +67,16 @@ app.use((req, res, next) => {
.json({ success: false, message: "Forbidden: Origin not allowed" });
});
// ---- 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
// ══════════════════════════════════════════════════════════════
+129 -5
View File
@@ -321,12 +321,136 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
}
});
// ══════════════════════════════════════════════════════════════
// CATEGORY ROUTES
// ══════════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════════
// CATEGORY ROUTES
// ══════════════════════════════════════════════════════════════
// NOTE: Category routes (genre, type, status) are registered separately
categoryRoutes(app, jsonResponse, jsonError);
// NOTE: Category routes (genre, type, status) are registered separately
categoryRoutes(app, jsonResponse, jsonError);
// ══════════════════════════════════════════════════════════════
// HEALTH & STATS
// ══════════════════════════════════════════════════════════════
const startTime = Date.now();
let requestCount = 0;
let errorCount = 0;
// Middleware to count requests
app.use((req, res, next) => {
if (req.path.startsWith("/api")) {
requestCount++;
res.on("finish", () => {
if (res.statusCode >= 400) errorCount++;
});
}
next();
});
// ---- FEATURE: Health check endpoint ----
app.get("/api/health", (req, res) => {
const uptime = Math.floor((Date.now() - startTime) / 1000);
const hours = Math.floor(uptime / 3600);
const minutes = Math.floor((uptime % 3600) / 60);
const seconds = uptime % 60;
res.json({
success: true,
results: {
status: "healthy",
version: "1.7.2",
uptime: `${hours}h ${minutes}m ${s}s`,
uptimeSeconds: uptime,
timestamp: new Date().toISOString(),
node: process.version,
memory: {
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + "MB",
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) + "MB",
},
},
});
});
// ---- FEATURE: Cache & API statistics ----
app.get("/api/stats", (req, res) => {
const uptime = Math.floor((Date.now() - startTime) / 1000);
res.json({
success: true,
results: {
uptime: uptime + "s",
requests: {
total: requestCount,
errors: errorCount,
successRate: requestCount > 0
? ((1 - errorCount / requestCount) * 100).toFixed(1) + "%"
: "100%",
},
cache: {
type: "in-memory",
ttl: "5 minutes",
description: "Map-based cache with TTL expiration",
},
endpoints: 27,
timestamp: new Date().toISOString(),
},
});
});
// ---- FEATURE: OpenAPI specification ----
app.get("/api/openapi", (req, res) => {
res.json({
openapi: "3.0.3",
info: {
title: "AniKotoAPI",
description: "Free REST API for anime data from anikototv.to",
version: "1.7.2",
contact: {
name: "Shinei Nouzen",
url: "https://github.com/Shineii86/AniKotoAPI",
},
},
servers: [
{ url: "https://anikototvapi.vercel.app/api", description: "Production" },
],
paths: {
"/": { get: { summary: "Homepage data", tags: ["Home"] } },
"/search": { get: { summary: "Search anime", tags: ["Search"] } },
"/info": { get: { summary: "Anime info", tags: ["Anime"] } },
"/episodes/{id}": { get: { summary: "Episode list", tags: ["Episodes"] } },
"/servers": { get: { summary: "Server list", tags: ["Streaming"] } },
"/stream": { get: { summary: "Stream URL", tags: ["Streaming"] } },
"/schedule": { get: { summary: "Anime schedule", tags: ["Schedule"] } },
"/random": { get: { summary: "Random anime", tags: ["Discovery"] } },
"/top-ten": { get: { summary: "Top 10 rankings", tags: ["Rankings"] } },
"/trending": { get: { summary: "Trending anime", tags: ["Discovery"] } },
"/spotlight": { get: { summary: "Spotlight anime", tags: ["Discovery"] } },
"/most-popular": { get: { summary: "Most popular", tags: ["Rankings"] } },
"/new-release": { get: { summary: "New releases", tags: ["Releases"] } },
"/filter": { get: { summary: "Advanced filter", tags: ["Filter"] } },
"/genre/{name}": { get: { summary: "By genre", tags: ["Categories"] } },
"/type/{name}": { get: { summary: "By type", tags: ["Categories"] } },
"/status/{name}": { get: { summary: "By status", tags: ["Categories"] } },
"/health": { get: { summary: "Health check", tags: ["System"] } },
"/stats": { get: { summary: "API statistics", tags: ["System"] } },
"/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } },
},
tags: [
{ name: "Home", description: "Homepage data" },
{ name: "Search", description: "Search & suggestions" },
{ name: "Anime", description: "Anime details" },
{ name: "Episodes", description: "Episode lists" },
{ name: "Streaming", description: "Stream URLs & servers" },
{ name: "Schedule", description: "Airing schedule" },
{ name: "Discovery", description: "Random, trending, spotlight" },
{ name: "Rankings", description: "Top 10, most popular" },
{ name: "Releases", description: "New releases" },
{ name: "Filter", description: "Advanced filtering" },
{ name: "Categories", description: "Genre, type, status" },
{ name: "System", description: "Health, stats, OpenAPI" },
],
});
});
};
export { createApiRoutes };
+70
View File
@@ -0,0 +1,70 @@
/*
* ======= • ======= • ======= • ======= • =======• =======
* AniKotoAPI — test.js
* Repository: https://github.com/Shineii86/AniKotoAPI
*
* @description
* Integration test suite for AniKotoAPI endpoints.
* Tests all major endpoints for correct response format.
*
* @author Shinei Nouzen
* @license MIT
* ======= • ======= • ======= • ======= • =======• =======
*/
const BASE = process.env.API_URL || "https://anikototvapi.vercel.app/api";
const tests = [
{ name: "Home", url: "/" },
{ name: "Search", url: "/search?keyword=naruto" },
{ name: "Info", url: "/info?id=road-of-naruto-ggjw8" },
{ name: "Episodes", url: "/episodes/road-of-naruto-ggjw8" },
{ name: "Suggestions", url: "/suggestions?keyword=naruto" },
{ name: "Spotlight", url: "/spotlight" },
{ name: "Trending", url: "/trending" },
{ name: "Top 10", url: "/top-ten" },
{ name: "Random", url: "/random" },
{ name: "Most Popular", url: "/most-popular" },
{ name: "New Release", url: "/new-release" },
{ name: "Schedule", url: "/schedule" },
{ name: "Genre", url: "/genre/action" },
{ name: "Type", url: "/type/tv" },
{ name: "Status", url: "/status/currently-airing" },
{ name: "Health", url: "/health" },
{ name: "Stats", url: "/stats" },
{ name: "OpenAPI", url: "/openapi" },
];
let passed = 0;
let failed = 0;
async function runTest(test) {
try {
const res = await fetch(`${BASE}${test.url}`);
const data = await res.json();
if (data.success === true && data.results) {
console.log(`✅ ${test.name}`);
passed++;
} else {
console.log(`❌ ${test.name} - Invalid response format`);
failed++;
}
} catch (error) {
console.log(`❌ ${test.name} - ${error.message}`);
failed++;
}
}
async function runAll() {
console.log(`\n🧪 Running ${tests.length} tests...\n`);
for (const test of tests) {
await runTest(test);
}
console.log(`\n📊 Results: ${passed} passed, ${failed} failed, ${tests.length} total\n`);
process.exit(failed > 0 ? 1 : 0);
}
runAll();