mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 02:45:10 +00:00
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:
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user