From 48eb0388273358182ac85141c53c573f20995d99 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:39:30 +0530 Subject: [PATCH 01/22] ci: update type n format workflow --- .../{lint-&-format-check.yml => type-n-format-check.yml} | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) rename .github/workflows/{lint-&-format-check.yml => type-n-format-check.yml} (74%) diff --git a/.github/workflows/lint-&-format-check.yml b/.github/workflows/type-n-format-check.yml similarity index 74% rename from .github/workflows/lint-&-format-check.yml rename to .github/workflows/type-n-format-check.yml index 741370a..c0925fe 100644 --- a/.github/workflows/lint-&-format-check.yml +++ b/.github/workflows/type-n-format-check.yml @@ -1,4 +1,4 @@ -name: "Lint and format check" +name: "Type and format check" on: push: @@ -22,8 +22,9 @@ jobs: - name: Install deps run: npm i - - name: Check lint - run: npm run lint + - name: Check types + # This step checks for TypeScript type errors + run: npm run typecheck - name: Check prettier run: npm run format:check From 4690452462ac62516e9aa2105ee62f868b2f6952 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:40:35 +0530 Subject: [PATCH 02/22] refactor: update api description --- Dockerfile | 2 +- public/index.html | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index f0bee79..e77c6e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ RUN npm run build FROM node:22-alpine as prod LABEL org.opencontainers.image.source=https://github.com/ghoshRitesh12/aniwatch-api -LABEL org.opencontainers.image.description="Node.js API for obtaining anime information from hianime.to" +LABEL org.opencontainers.image.description="NodeJS API for obtaining anime data from hianime.to" LABEL org.opencontainers.image.licenses=MIT # install curl for healthcheck diff --git a/public/index.html b/public/index.html index c095416..8ade1a4 100644 --- a/public/index.html +++ b/public/index.html @@ -23,13 +23,13 @@ - + - - + + @@ -202,11 +202,11 @@ From 3602ba58026c8288ee49afa9deaea257766556f7 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:44:06 +0530 Subject: [PATCH 03/22] refactor: update title --- public/index.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 8ade1a4..cc737db 100644 --- a/public/index.html +++ b/public/index.html @@ -216,9 +216,7 @@ IMPORTANT:

- Welcome to the unofficial - hianimez.to - API + Welcome to the Aniwatch API ⚔️

From 74c9ffcb8f62dcdae6983da490436c39b6b8e558 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:45:26 +0530 Subject: [PATCH 04/22] feat: update response to include status instead of success for consistency --- src/routes/hianime.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/routes/hianime.ts b/src/routes/hianime.ts index 38242c0..0890612 100644 --- a/src/routes/hianime.ts +++ b/src/routes/hianime.ts @@ -1,10 +1,10 @@ import { Hono } from "hono"; import { HiAnime } from "aniwatch"; import { cache } from "../config/cache.js"; -import type { AniwatchAPIVariables } from "../config/variables.js"; +import type { ServerContext } from "../config/variables.js"; const hianime = new HiAnime.Scraper(); -const hianimeRouter = new Hono<{ Variables: AniwatchAPIVariables }>(); +const hianimeRouter = new Hono(); // /api/v2/hianime hianimeRouter.get("/", (c) => c.redirect("/", 301)); @@ -19,7 +19,7 @@ hianimeRouter.get("/home", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/azlist/{sortOption}?page={page} @@ -38,7 +38,7 @@ hianimeRouter.get("/azlist/:sortOption", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/qtip/{animeId} @@ -52,7 +52,7 @@ hianimeRouter.get("/qtip/:animeId", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/category/{name}?page={page} @@ -70,7 +70,7 @@ hianimeRouter.get("/category/:name", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/genre/{name}?page={page} @@ -86,7 +86,7 @@ hianimeRouter.get("/genre/:name", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/producer/{name}?page={page} @@ -102,7 +102,7 @@ hianimeRouter.get("/producer/:name", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/schedule?date={date}&tzOffset={tzOffset} @@ -121,7 +121,7 @@ hianimeRouter.get("/schedule", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/search?q={query}&page={page}&filters={...filters} @@ -138,7 +138,7 @@ hianimeRouter.get("/search", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/search/suggestion?q={query} @@ -152,7 +152,7 @@ hianimeRouter.get("/search/suggestion", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/anime/{animeId} @@ -166,7 +166,7 @@ hianimeRouter.get("/anime/:animeId", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/episode/servers?animeEpisodeId={id} @@ -182,7 +182,7 @@ hianimeRouter.get("/episode/servers", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // episodeId=steinsgate-3?ep=230 @@ -206,7 +206,7 @@ hianimeRouter.get("/episode/sources", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/anime/{anime-id}/episodes @@ -220,7 +220,7 @@ hianimeRouter.get("/anime/:animeId/episodes", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); // /api/v2/hianime/anime/{anime-id}/next-episode-schedule @@ -234,7 +234,7 @@ hianimeRouter.get("/anime/:animeId/next-episode-schedule", async (c) => { cacheConfig.duration ); - return c.json({ success: true, data }, { status: 200 }); + return c.json({ status: 200, data }, { status: 200 }); }); export { hianimeRouter }; From 013196bd3d5e44aa76b1825f24f9f5186fe7e7c1 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:46:06 +0530 Subject: [PATCH 05/22] feat: add cache response header if caching is enabled --- src/middleware/cache.ts | 45 +++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/middleware/cache.ts b/src/middleware/cache.ts index 164f4a3..8fd3dc8 100644 --- a/src/middleware/cache.ts +++ b/src/middleware/cache.ts @@ -1,6 +1,8 @@ -import type { MiddlewareHandler } from "hono"; import { env } from "../config/env.js"; -import { AniwatchAPICache } from "../config/cache.js"; +import { AniwatchAPICache, cache } from "../config/cache.js"; +import type { BlankInput } from "hono/types"; +import type { Context, MiddlewareHandler } from "hono"; +import type { ServerContext } from "../config/variables.js"; // Define middleware to add Cache-Control header export const cacheControl: MiddlewareHandler = async (c, next) => { @@ -19,14 +21,45 @@ export function cacheConfigSetter(keySliceIndex: number): MiddlewareHandler { return async (c, next) => { const { pathname, search } = new URL(c.req.url); + const duration = Number( + c.req.header(AniwatchAPICache.CACHE_EXPIRY_HEADER_NAME) || + AniwatchAPICache.DEFAULT_CACHE_EXPIRY_SECONDS + ); c.set("CACHE_CONFIG", { key: `${pathname.slice(keySliceIndex) + search}`, - duration: Number( - c.req.header(AniwatchAPICache.CACHE_EXPIRY_HEADER_NAME) || - AniwatchAPICache.DEFAULT_CACHE_EXPIRY_SECONDS - ), + duration, }); + if (AniwatchAPICache.enabled) { + c.res.headers.set( + AniwatchAPICache.CACHE_EXPIRY_HEADER_NAME, + duration.toString() + ); + } + await next(); }; } + +export function withCache( + getData: (c: Context) => Promise +) { + return async (c: Context) => { + const cacheConfig = c.get("CACHE_CONFIG"); + + const data = await cache.getOrSet( + () => getData(c), + cacheConfig.key, + cacheConfig.duration + ); + + return c.json({ status: 200, data }, { status: 200 }); + }; +} + +// export function _withCache( +// context: Context, +// promise: Promise +// ): MiddlewareHandler { +// return async (c) => {}; +// } From 1017371432f2b2fca825eadbcc8b68c4af4e3d1e Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:47:09 +0530 Subject: [PATCH 06/22] refactor: add `quitConnection` method; update variable names, function defs & logic --- src/config/cache.ts | 49 +++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/config/cache.ts b/src/config/cache.ts index 584ff69..770cce0 100644 --- a/src/config/cache.ts +++ b/src/config/cache.ts @@ -5,15 +5,17 @@ export class AniwatchAPICache { private static instance: AniwatchAPICache | null = null; private client: Redis | null; - public isOptional: boolean = true; + public enabled: boolean = false; - static DEFAULT_CACHE_EXPIRY_SECONDS = 60 as const; - static CACHE_EXPIRY_HEADER_NAME = "X-ANIWATCH-CACHE-EXPIRY" as const; + static enabled = false; + // 5 mins, 5 * 60 + static DEFAULT_CACHE_EXPIRY_SECONDS = 300 as const; + static CACHE_EXPIRY_HEADER_NAME = "Aniwatch-Cache-Expiry" as const; constructor() { const redisConnURL = env.ANIWATCH_API_REDIS_CONN_URL; - this.isOptional = !Boolean(redisConnURL); - this.client = this.isOptional ? null : new Redis(String(redisConnURL)); + this.enabled = AniwatchAPICache.enabled = Boolean(redisConnURL); + this.client = this.enabled ? new Redis(String(redisConnURL)) : null; } static getInstance() { @@ -23,27 +25,17 @@ export class AniwatchAPICache { return AniwatchAPICache.instance; } - // set(key: string | Buffer, value: string | Buffer | number) { - // if (this.isOptional) return; - // return this.client?.set(key, value); - // } - - // get(key: string | Buffer) { - // if (this.isOptional) return; - // return this.client?.get(key); - // } - /** - * @param expirySeconds set to 60 by default + * @param expirySeconds set to 300 (5 mins) by default */ async getOrSet( dataGetter: () => Promise, - key: string | Buffer, + key: string, expirySeconds: number = AniwatchAPICache.DEFAULT_CACHE_EXPIRY_SECONDS ) { - const cachedData = this.isOptional - ? null - : (await this.client?.get?.(key)) || null; + const cachedData = this.enabled + ? (await this.client?.get?.(key)) || null + : null; let data = JSON.parse(String(cachedData)) as T; if (!data) { @@ -57,6 +49,23 @@ export class AniwatchAPICache { } return data; } + + closeConnection() { + this.client + ?.quit() + ?.then(() => { + this.client = null; + AniwatchAPICache.instance = null; + console.info( + "aniwatch-api redis connection closed and cache instance reset" + ); + }) + .catch((err) => { + console.error( + `aniwatch-api error while closing redis connection: ${err}` + ); + }); + } } export const cache = AniwatchAPICache.getInstance(); From b19078e29cbd86d46d4ef1c5e5c6e914d1c8cc2f Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:48:39 +0530 Subject: [PATCH 07/22] feat: add graceful shutdown and better deployment environment logic --- src/server.ts | 98 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/src/server.ts b/src/server.ts index 15bc93e..0cd6d53 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,12 +4,13 @@ import { Hono } from "hono"; import { serve } from "@hono/node-server"; import { serveStatic } from "@hono/node-server/serve-static"; -import { env } from "./config/env.js"; import { log } from "./config/logger.js"; import { corsConfig } from "./config/cors.js"; import { ratelimit } from "./config/ratelimit.js"; +import { execGracefulShutdown } from "./utils.js"; +import { DeploymentEnv, env, SERVERLESS_ENVIRONMENTS } from "./config/env.js"; import { errorHandler, notFoundHandler } from "./config/errorHandler.js"; -import type { AniwatchAPIVariables } from "./config/variables.js"; +import type { ServerContext } from "./config/variables.js"; import { hianimeRouter } from "./routes/hianime.js"; import { logging } from "./middleware/logging.js"; @@ -20,22 +21,30 @@ import pkgJson from "../package.json" with { type: "json" }; // const BASE_PATH = "/api/v2" as const; -const app = new Hono<{ Variables: AniwatchAPIVariables }>(); +const app = new Hono(); app.use(logging); app.use(corsConfig); app.use(cacheControl); -// -// CAUTION: For personal deployments, "refrain" from having an env -// named "ANIWATCH_API_HOSTNAME". You may face rate limitting -// or other issues if you do. -const ISNT_PERSONAL_DEPLOYMENT = Boolean(env.ANIWATCH_API_HOSTNAME); -if (ISNT_PERSONAL_DEPLOYMENT) { +/* + CAUTION: + Having the "ANIWATCH_API_HOSTNAME" env will + enable rate limitting for the deployment. + WARNING: + If you are using any serverless environment, you must set the + "ANIWATCH_API_DEPLOYMENT_ENV" to that environment's name, + otherwise you may face issues. +*/ +const isPersonalDeployment = Boolean(env.ANIWATCH_API_HOSTNAME); +if (isPersonalDeployment) { app.use(ratelimit); } -app.use("/", serveStatic({ root: "public" })); +if (env.ANIWATCH_API_DEPLOYMENT_ENV === DeploymentEnv.NODEJS) { + app.use("/", serveStatic({ root: "public" })); +} + app.get("/health", (c) => c.text("daijoubu", { status: 200 })); app.get("/v", async (c) => c.text( @@ -55,31 +64,72 @@ app.notFound(notFoundHandler); app.onError(errorHandler); // -// NOTE: this env is "required" for vercel deployments -if (!env.ANIWATCH_API_VERCEL_DEPLOYMENT) { - serve({ +(function () { + /* + NOTE: + "ANIWATCH_API_DEPLOYMENT_MODE" env must be set to + its supported name for serverless deployments + Eg: "vercel" for vercel deployments + */ + if (SERVERLESS_ENVIRONMENTS.includes(env.ANIWATCH_API_DEPLOYMENT_ENV)) { + return; + } + + const server = serve({ port: env.ANIWATCH_API_PORT, fetch: app.fetch, - }).addListener("listening", () => { + }).addListener("listening", () => log.info( `aniwatch-api RUNNING at http://localhost:${env.ANIWATCH_API_PORT}` + ) + ); + + process.on("SIGINT", () => execGracefulShutdown(server)); + process.on("SIGTERM", () => execGracefulShutdown(server)); + process.on("uncaughtException", (err) => { + log.error(`Uncaught Exception: ${err.message}`); + execGracefulShutdown(server); + }); + process.on("unhandledRejection", (reason, promise) => { + log.error( + `Unhandled Rejection at: ${promise}, reason: ${reason instanceof Error ? reason.message : reason}` ); + execGracefulShutdown(server); }); - // NOTE: remove the `if` block below for personal deployments - if (ISNT_PERSONAL_DEPLOYMENT) { - const interval = 9 * 60 * 1000; // 9mins + /* + CAUTION: + The `if` below block is for `render free deployments` only, + as their free tier has an approx 10 or 15 minute sleep time. + This is to keep the server awake and prevent it from sleeping. + You can enable the automatic health check by setting the + environment variables "ANIWATCH_API_HOSTNAME" to your deployment's hostname, + and "ANIWATCH_API_DEPLOYMENT_ENV" to "render" in your environment variables. + If you are not using render, you can remove the below `if` block. + */ + if ( + isPersonalDeployment && + env.ANIWATCH_API_DEPLOYMENT_ENV === DeploymentEnv.RENDER + ) { + const INTERVAL_DELAY = 8 * 60 * 1000; // 8mins + const url = new URL(`https://${env.ANIWATCH_API_HOSTNAME}/health`); // don't sleep setInterval(() => { - log.info( - `aniwatch-api HEALTH_CHECK at ${new Date().toISOString()}` - ); https - .get(`https://${env.ANIWATCH_API_HOSTNAME}/health`) - .on("error", (err) => log.error(err.message.trim())); - }, interval); + .get(url.href) + .on("response", () => { + log.info( + `aniwatch-api HEALTH_CHECK at ${new Date().toISOString()}` + ); + }) + .on("error", (err) => + log.warn( + `aniwatch-api HEALTH_CHECK failed; ${err.message.trim()}` + ) + ); + }, INTERVAL_DELAY); } -} +})(); export default app; From 536b97e549675e1f0b035a5139320d6d60780608 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:49:05 +0530 Subject: [PATCH 08/22] feat: add graceful shutdown and better deployment environment logic --- src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 0cd6d53..8ff1e03 100644 --- a/src/server.ts +++ b/src/server.ts @@ -10,7 +10,7 @@ import { ratelimit } from "./config/ratelimit.js"; import { execGracefulShutdown } from "./utils.js"; import { DeploymentEnv, env, SERVERLESS_ENVIRONMENTS } from "./config/env.js"; import { errorHandler, notFoundHandler } from "./config/errorHandler.js"; -import type { ServerContext } from "./config/variables.js"; +import type { ServerContext } from "./config/context.js"; import { hianimeRouter } from "./routes/hianime.js"; import { logging } from "./middleware/logging.js"; From 83035e46ce57f62637edb8f164b3a6fcf722f3c1 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:49:30 +0530 Subject: [PATCH 09/22] refactor: rename variables to context.ts --- src/config/context.ts | 12 ++++++++++++ src/config/variables.ts | 8 -------- 2 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 src/config/context.ts delete mode 100644 src/config/variables.ts diff --git a/src/config/context.ts b/src/config/context.ts new file mode 100644 index 0000000..4b76b63 --- /dev/null +++ b/src/config/context.ts @@ -0,0 +1,12 @@ +import type { Prettify } from "../utils.js"; + +type ServerContextVariables = Prettify<{ + CACHE_CONFIG: { + key: string; + duration: number; + }; +}>; + +export type ServerContext = Prettify<{ + Variables: ServerContextVariables; +}>; diff --git a/src/config/variables.ts b/src/config/variables.ts deleted file mode 100644 index dc90dcf..0000000 --- a/src/config/variables.ts +++ /dev/null @@ -1,8 +0,0 @@ -type CacheVariables = { - CACHE_CONFIG: { - key: string; - duration: number; - }; -}; - -export type AniwatchAPIVariables = CacheVariables & {}; From a3cd5a34aada2331e3f9bd56798f1ddb979dbb01 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:50:40 +0530 Subject: [PATCH 10/22] feat: add `execGracefulShutdown` method & `Prettify` type --- src/utils.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/utils.ts diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..64965ae --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,18 @@ +import { cache } from "./config/cache.js"; +import type { ServerType } from "@hono/node-server"; + +export type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +export function execGracefulShutdown(server: ServerType) { + process.stdout.write("\naniwatch-api SHUTTING DOWN gracefully...\n"); + + cache.closeConnection(); + server.close((err) => { + process.stdout.write("\naniwatch-api SHUTDOWN complete.\n"); + err ? console.error(err) : null; + process.exit(err ? 1 : 0); + }); + process.exit(0); +} From 5118a2543dbd5e2cf23478917cfb848cceffad61 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:51:12 +0530 Subject: [PATCH 11/22] refactor: `ServerContext` type import --- src/routes/hianime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/hianime.ts b/src/routes/hianime.ts index 0890612..49138eb 100644 --- a/src/routes/hianime.ts +++ b/src/routes/hianime.ts @@ -1,7 +1,7 @@ import { Hono } from "hono"; import { HiAnime } from "aniwatch"; import { cache } from "../config/cache.js"; -import type { ServerContext } from "../config/variables.js"; +import type { ServerContext } from "../config/context.js"; const hianime = new HiAnime.Scraper(); const hianimeRouter = new Hono(); From ce03d5939d4ae3f69cf7daa8b19fe1df42ff772d Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:51:41 +0530 Subject: [PATCH 12/22] refactor: `ServerContext` type import --- src/middleware/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/cache.ts b/src/middleware/cache.ts index 8fd3dc8..0b80dc9 100644 --- a/src/middleware/cache.ts +++ b/src/middleware/cache.ts @@ -2,7 +2,7 @@ import { env } from "../config/env.js"; import { AniwatchAPICache, cache } from "../config/cache.js"; import type { BlankInput } from "hono/types"; import type { Context, MiddlewareHandler } from "hono"; -import type { ServerContext } from "../config/variables.js"; +import type { ServerContext } from "../config/context.js"; // Define middleware to add Cache-Control header export const cacheControl: MiddlewareHandler = async (c, next) => { From bd597a6fd71347c9cef687a891a408f40e6cbd93 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:52:35 +0530 Subject: [PATCH 13/22] feat: add flexible `ANIWATCH_API_DEPLOYMENT_ENV` env and deployment envs --- src/config/env.ts | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/config/env.ts b/src/config/env.ts index af7172d..8263b13 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -1,8 +1,23 @@ import { config } from "dotenv"; -import { cleanEnv, num, str, bool, url, port } from "envalid"; +import { cleanEnv, num, str, url, port } from "envalid"; config(); +export enum DeploymentEnv { + NODEJS = "nodejs", + DOCKER = "docker", + VERCEL = "vercel", + CLOUDFLARE_WORKERS = "cloudflare-workers", + RENDER = "render", +} +export const API_DEPLOYMENT_ENVIRONMENTS = Object.values(DeploymentEnv); +export const SERVERLESS_ENVIRONMENTS = [ + DeploymentEnv.VERCEL, + DeploymentEnv.CLOUDFLARE_WORKERS, + DeploymentEnv.RENDER, +]; + +// export const env = cleanEnv(process.env, { ANIWATCH_API_PORT: port({ default: 4000, @@ -28,9 +43,11 @@ export const env = cleanEnv(process.env, { desc: "Allowed origins, separated by commas and no spaces in between (CSV).", }), - ANIWATCH_API_VERCEL_DEPLOYMENT: bool({ - default: false, - desc: "Required for distinguishing Vercel deployment from other ones; set it to true", + ANIWATCH_API_DEPLOYMENT_ENV: str({ + choices: API_DEPLOYMENT_ENVIRONMENTS, + default: DeploymentEnv.NODEJS, + example: DeploymentEnv.VERCEL, + desc: "The deployment environment of the Aniwatch API. It must be set incase of serverless deployments, can lead to issues if not set.", }), ANIWATCH_API_HOSTNAME: str({ @@ -72,7 +89,3 @@ function isDevEnv(): boolean { process.env.NODE_ENV === "test" ); } - -export function isEnvUndefined(envVar?: string): boolean { - return typeof envVar === "undefined" || envVar === "undefined"; -} From f5d74627e8b7f844f58a9dc82e3a79ecd911cc8f Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:53:29 +0530 Subject: [PATCH 14/22] feat: add deployment env based rate limiters --- src/config/ratelimit.ts | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/config/ratelimit.ts b/src/config/ratelimit.ts index 8bfc308..06b6ef4 100644 --- a/src/config/ratelimit.ts +++ b/src/config/ratelimit.ts @@ -1,11 +1,28 @@ import { rateLimiter } from "hono-rate-limiter"; -import { getConnInfo } from "@hono/node-server/conninfo"; -import { env } from "./env.js"; +import { getConnInfo as vercelGetConnInfo } from "hono/vercel"; +import { getConnInfo as cfWorkersGetConnInfo } from "hono/cloudflare-workers"; +import { getConnInfo as nodejsGetConnInfo } from "@hono/node-server/conninfo"; +import type { GetConnInfo } from "hono/conninfo"; +import { DeploymentEnv, env } from "./env.js"; + +let getConnInfo: GetConnInfo; +switch (env.ANIWATCH_API_DEPLOYMENT_ENV) { + case DeploymentEnv.VERCEL: + getConnInfo = vercelGetConnInfo; + break; + case DeploymentEnv.CLOUDFLARE_WORKERS: + getConnInfo = cfWorkersGetConnInfo; + break; + default: + getConnInfo = nodejsGetConnInfo; + break; +} export const ratelimit = rateLimiter({ - windowMs: env.ANIWATCH_API_WINDOW_MS, - limit: env.ANIWATCH_API_MAX_REQS, standardHeaders: "draft-7", + limit: env.ANIWATCH_API_MAX_REQS, + windowMs: env.ANIWATCH_API_WINDOW_MS, + keyGenerator(c) { const { remote } = getConnInfo(c); const key = @@ -14,9 +31,10 @@ export const ratelimit = rateLimiter({ return key; }, - handler: (c) => - c.json( + handler(c) { + return c.json( { status: 429, message: "Too Many Requests 😵" }, { status: 429 } - ), + ); + }, }); From cdd05c719ba361cc28acf6c4a7d535c64a70e604 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:54:19 +0530 Subject: [PATCH 15/22] feat: update imp scripts, description & packages --- package.json | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index e4ac952..300771d 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { "name": "aniwatch-api", "version": "2.14.5", - "description": "Node.js API for obtaining anime information from hianimez.to", - "main": "src/server.ts", + "description": "NodeJS API for obtaining anime data from hianimez.to", + "main": "dist/src/server.js", "type": "module", "scripts": { - "start": "tsx src/server.ts", + "start": "node dist/src/server.js", "dev": "tsx watch src/server.ts", - "build": "tsc -p tsconfig.json", - "vercel-build": "echo \"Hello\"", + "build": "tsc", + "buildnstart": "tsc && node dist/src/server.js", + "vercel-build": "echo hello", "prepare": "node .husky/install.mjs", "test": "vitest run --config vitest.config.ts", "healthcheck": "curl -f http://localhost:4000/health", - "lint": "tsc", + "typecheck": "tsc --noEmit", "format": "prettier --cache --write .", "format:check": "prettier --cache --check ." }, @@ -36,22 +37,22 @@ "author": "https://github.com/ghoshRitesh12", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.14.1", + "@hono/node-server": "^1.14.2", "aniwatch": "^2.23.0", "dotenv": "^16.5.0", "envalid": "^8.0.0", - "hono": "^4.7.9", + "hono": "^4.7.10", "hono-rate-limiter": "^0.4.2", "ioredis": "^5.6.1", - "pino": "^9.6.0", - "tsx": "^4.19.4" + "pino": "^9.7.0" }, "devDependencies": { - "@types/node": "^22.15.17", + "@types/node": "^22.15.21", "husky": "^9.1.7", "pino-pretty": "^13.0.0", "prettier": "^3.5.3", + "tsx": "^4.19.4", "typescript": "^5.8.3", - "vitest": "^3.1.3" + "vitest": "^3.1.4" } } \ No newline at end of file From 8110f1af72ee7892aa39c0e56606df78fd7a4d5e Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 22:58:05 +0530 Subject: [PATCH 16/22] refactor: remove unnecessary break --- src/config/ratelimit.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config/ratelimit.ts b/src/config/ratelimit.ts index 06b6ef4..8374d4f 100644 --- a/src/config/ratelimit.ts +++ b/src/config/ratelimit.ts @@ -15,7 +15,6 @@ switch (env.ANIWATCH_API_DEPLOYMENT_ENV) { break; default: getConnInfo = nodejsGetConnInfo; - break; } export const ratelimit = rateLimiter({ From 18cfcb8d06a0a9dac722ead510c728a04e906a02 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 23:58:05 +0530 Subject: [PATCH 17/22] docs: update readme --- README.md | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1f16b59..944cfb3 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ > [!IMPORTANT] > -> 1. [https://api-aniwatch.onrender.com](https://api-aniwatch.onrender.com/) is only meant to demo the API and has rate-limiting enabled to minimize bandwidth consumption. It is recommended to deploy your own instance for personal use by customizing the API as you need it to be. +> 1. There was previously a hosted version of this API for showcasing purposes only, and it was misused; since then, there have been no other hosted versions. It is recommended to deploy your own instance for personal use by customizing the API as you need it to be. > 2. This API is just an unofficial API for [hianimez.to](https://hianimez.to) and is in no other way officially related to the same. > 3. The content that this API provides is not mine, nor is it hosted by me. These belong to their respective owners. This API just demonstrates how to build an API that scrapes websites and uses their content. @@ -137,21 +137,21 @@ The `-d` flag runs the container in detached mode, and the `--name` flag is used Currently this API supports parsing of only one custom header, and more may be implemented in the future to accommodate varying needs. -- `X-ANIWATCH-CACHE-EXPIRY`: this custom header is used to specify the cache expiration duration in **seconds** (defaults to 60 if the header is missing). The `ANIWATCH_API_REDIS_CONN_URL` env is required for this custom header to function as intended; otherwise, there's no point in setting this custom header. +- `Aniwatch-Cache-Expiry`: This custom request header is used to specify the cache expiration duration in **seconds** (defaults to 300 or 5 mins if the header is missing). The `ANIWATCH_API_REDIS_CONN_URL` env is required for this custom header to function as intended; otherwise, there's no point in setting this custom request header. A **response header** of the same name is also returned with the value set to the cache expiration duration in seconds if `ANIWATCH_API_REDIS_CONN_URL` env is set. ### Environment Variables More info can be found in the [`.env.example`](https://github.com/ghoshRitesh12/aniwatch-api/blob/main/.env.example) file, where envs' having a value that is contained within `<` `>` angled brackets, commented out or not, are just examples and should be replaced with relevant ones. -- `ANIWATCH_API_PORT`: port number of the aniwatch API. -- `ANIWATCH_API_WINDOW_MS`: duration to track requests for rate limiting (in milliseconds). -- `ANIWATCH_API_MAX_REQS`: maximum number of requests in the `ANIWATCH_API_WINDOW_MS` time period. -- `ANIWATCH_API_CORS_ALLOWED_ORIGINS`: allowed origins, separated by commas and no spaces in between. -- `ANIWATCH_API_VERCEL_DEPLOYMENT`: required for distinguishing Vercel deployment from other ones; set it to true or any other non-zero value. -- `ANIWATCH_API_HOSTNAME`: set this to your api instance's hostname to enable rate limiting, don't have this value if you don't wish to rate limit. -- `ANIWATCH_API_REDIS_CONN_URL`: this env is optional by default and can be set to utilize Redis caching functionality. It has to be a valid connection URL; otherwise, the Redis client can throw unexpected errors. -- `ANIWATCH_API_S_MAXAGE`: specifies the maximum amount of time (in seconds) a resource is considered fresh when served by a CDN cache. -- `ANIWATCH_API_STALE_WHILE_REVALIDATE`: specifies the amount of time (in seconds) a resource is served stale while a new one is fetched. +- `ANIWATCH_API_PORT`: Port number of the aniwatch API. +- `ANIWATCH_API_WINDOW_MS`: Duration to track requests for rate limiting (in milliseconds). +- `ANIWATCH_API_MAX_REQS`: Maximum number of requests in the `ANIWATCH_API_WINDOW_MS` time period. +- `ANIWATCH_API_CORS_ALLOWED_ORIGINS`: Allowed origins, separated by commas and no spaces in between (CSV). +- `ANIWATCH_API_DEPLOYMENT_ENV`: The deployment environment of the Aniwatch API. Many configurations depend on this env, rate limiting being one of them. It must be set incase of serverless deployments; otherwise, you may run into weird issues. Possible values: `'nodejs' | 'docker' | 'vercel' | 'render' | 'cloudflare-workers'`. +- `ANIWATCH_API_HOSTNAME`: Set this to your api instance's hostname to enable rate limiting, don't have this value if you don't wish to rate limit. +- `ANIWATCH_API_REDIS_CONN_URL`: This env is optional by default and can be set to utilize Redis caching functionality. It has to be a valid connection URL; otherwise, the Redis client can throw unexpected errors. +- `ANIWATCH_API_S_MAXAGE`: Specifies the maximum amount of time (in seconds) a resource is considered fresh when served by a CDN cache. +- `ANIWATCH_API_STALE_WHILE_REVALIDATE`: Specifies the amount of time (in seconds) a resource is served stale while a new one is fetched. ## ⛅ Host your instance @@ -161,22 +161,27 @@ More info can be found in the [`.env.example`](https://github.com/ghoshRitesh12/ > > - If you want to have rate limiting in your application, then set the `ANIWATCH_API_HOSTNAME` env to your deployed instance's hostname; otherwise, don't set or have this env at all. If you set this env to an incorrect value, you may face other issues. > - It's optional by default, but if you want to have endpoint response caching functionality, then set the `ANIWATCH_API_REDIS_CONN_URL` env to a valid Redis connection URL. If the connection URL is invalid, the Redis client can throw unexpected errors. -> - Remove the if block from the [`server.ts`](https://github.com/ghoshRitesh12/aniwatch-api/blob/main/src/server.ts) file, spanning from lines [61](https://github.com/ghoshRitesh12/aniwatch-api/blob/main/src/server.ts#L61) to [85](https://github.com/ghoshRitesh12/aniwatch-api/blob/main/src/server.ts#L85). +> - You **may or may not** wanna remove the last `if` block within the [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) in the [`server.ts`](https://github.com/ghoshRitesh12/aniwatch-api/blob/main/src/server.ts) file. It is for **render free deployments** only, as their free tier has an approx 10 or 15 minute sleep time. That `if` block keeps the server awake and prevents it from sleeping. You can enable the automatic health check by setting the environment variables `ANIWATCH_API_HOSTNAME` to your deployment's hostname, and `ANIWATCH_API_DEPLOYMENT_ENV` to `render` in your environment variables. If you are not using render, you can remove that `if` block. +> - If you are using a serverless deployment, then set the `ANIWATCH_API_DEPLOYMENT_ENV` env to `vercel` or `render` or `cloudflare-workers` depending on your deployment platform. This is because the API uses this env to configure different functionalities, such as rate limiting, graceful shutdown or hosting static files. ### Vercel Deploy your own instance of Aniwatch API on Vercel. -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/ghoshRitesh12/aniwatch-api) - > [!NOTE] > -> When deploying to vercel, set an env named `ANIWATCH_API_VERCEL_DEPLOYMENT` to `true` or any non-zero value, but this env must be present. +> When deploying to vercel, you must set the env named `ANIWATCH_API_DEPLOYMENT_ENV` to `vercel` for proper functioning of the API. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/ghoshRitesh12/aniwatch-api) ### Render Deploy your own instance of Aniwatch API on Render. +> [!NOTE] +> +> When deploying to vercel, you must set the env named `ANIWATCH_API_DEPLOYMENT_ENV` to `render` for proper functioning of the API. + [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/ghoshRitesh12/aniwatch-api) ## 📚 Documentation From d9fa1c66a1238558755bb559354a6e2db2e4976f Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 23:58:16 +0530 Subject: [PATCH 18/22] refactor: update index.html page --- public/index.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/index.html b/public/index.html index cc737db..7605e4e 100644 --- a/public/index.html +++ b/public/index.html @@ -202,11 +202,12 @@ From f75b5616b4d2a1e334731863343ad89d9ef69471 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 23:58:36 +0530 Subject: [PATCH 19/22] refactor: update .env.example file --- .env.example | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index f3b121e..d9ba7fd 100644 --- a/.env.example +++ b/.env.example @@ -18,8 +18,12 @@ ANIWATCH_API_MAX_REQS=70 # ANIWATCH_API_HOSTNAME= -# NOTE: this env is "required" for vercel deployments -# ANIWATCH_API_VERCEL_DEPLOYMENT= +# NOTE: +# The deployment environment of the Aniwatch API. +# Many configurations depend on this env, rate limiting being one of them. +# It must be set incase of serverless deployments; otherwise, you may run into weird issues. +# Possible values: 'nodejs' | 'docker' | 'vercel' | 'render' | 'cloudflare-workers' +ANIWATCH_API_DEPLOYMENT_ENV="nodejs" # env to use optional redis caching functionality ANIWATCH_API_REDIS_CONN_URL= From b0cc46f53eb226d99eb0a6e7a9f00fcf80c16c5d Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Sun, 25 May 2025 23:59:20 +0530 Subject: [PATCH 20/22] refactor: add docs link & more description for `ANIWATCH_API_DEPLOYMENT_ENV` env --- src/config/env.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/config/env.ts b/src/config/env.ts index 8263b13..2935084 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -47,7 +47,8 @@ export const env = cleanEnv(process.env, { choices: API_DEPLOYMENT_ENVIRONMENTS, default: DeploymentEnv.NODEJS, example: DeploymentEnv.VERCEL, - desc: "The deployment environment of the Aniwatch API. It must be set incase of serverless deployments, can lead to issues if not set.", + desc: "The deployment environment of the Aniwatch API. It must be set incase of serverless deployments, otherwise you may run into weird issues.", + docs: "https://github.com/ghoshRitesh12/aniwatch-api/blob/main/.env.example#L21", }), ANIWATCH_API_HOSTNAME: str({ @@ -61,7 +62,7 @@ export const env = cleanEnv(process.env, { default: undefined, example: "rediss://default:your-secure-password@your-redis-instance-name.provider.com:6379", - desc: "This env is optional by default and can be set to utilize Redis caching functionality. It has to be a valid connection URL.", + desc: "This env is optional by default and can be set to utilize Redis caching functionality. It has to be a valid connection URL; otherwise, the Redis client can throw unexpected errors", }), ANIWATCH_API_S_MAXAGE: num({ From e108ce6a4ee0968d4e68f748543cce6c69e22958 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Mon, 26 May 2025 00:00:18 +0530 Subject: [PATCH 21/22] docs: update oopsie --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 944cfb3..6408172 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ Deploy your own instance of Aniwatch API on Render. > [!NOTE] > -> When deploying to vercel, you must set the env named `ANIWATCH_API_DEPLOYMENT_ENV` to `render` for proper functioning of the API. +> When deploying to render, you must set the env named `ANIWATCH_API_DEPLOYMENT_ENV` to `render` for proper functioning of the API. [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/ghoshRitesh12/aniwatch-api) From 113945c53a75ddc7b52990d3f1a95f953ab205d7 Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh Date: Mon, 26 May 2025 00:03:58 +0530 Subject: [PATCH 22/22] refactor: un-feature-gate static content hosting depending on deployment env --- src/server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server.ts b/src/server.ts index 8ff1e03..efafb72 100644 --- a/src/server.ts +++ b/src/server.ts @@ -41,9 +41,9 @@ if (isPersonalDeployment) { app.use(ratelimit); } -if (env.ANIWATCH_API_DEPLOYMENT_ENV === DeploymentEnv.NODEJS) { - app.use("/", serveStatic({ root: "public" })); -} +// if (env.ANIWATCH_API_DEPLOYMENT_ENV === DeploymentEnv.NODEJS) { +app.use("/", serveStatic({ root: "public" })); +// } app.get("/health", (c) => c.text("daijoubu", { status: 200 })); app.get("/v", async (c) =>