Architecture
The app/worker split
This repo ships two separate services that together answer a /v1 request:
apps/api(this directory) — a Node/Hono service. It handles auth, credits, rate limiting, request/response shaping, and talks to two kinds of upstream: third-party APIs (RapidAPI, TikHub) called directly, and this repo's own Python worker for sites that need a stateful, proxied browser session.apps/worker— a Python/FastAPI service (apps/worker/src/app.py). It owns proxy pools, session persistence, and anti-bot handling for sites that can't be scraped with a bare HTTP request — e.g. Etsy runs DataDome, so a real proxied browser has to mint a validated session before fast HTTP fetches can use it (see the Etsy tag description inapps/api/src/app.ts).
A site's service file picks one path. Sites that go through the worker call buildUrl(path, params) (apps/api/src/utils/buildUrl.ts), which resolves against WORKER_HOST — see apps/api/src/routes/walmart/walmart.service.ts, ebay.service.ts, etsy.service.ts, pinterest.service.ts, meta.service.ts, instagram.service.ts, threads.service.ts, linkedin.service.ts, rednote.service.ts for examples. Sites backed by RapidAPI or TikHub call out directly via UpstreamHandler subclasses (below), never touching WORKER_HOST.
Vantage's background scans (apps/api/src/jobs/) also go through the worker rather than a metered vendor — see the policy comment at the top of apps/api/src/routes/app/app.credits.ts.
Why packages/ exists
packages/ (@thenetaji/packages) is shared TypeScript code used by apps/api only — not apps/web, and not apps/worker (which is Python). It holds three things apps/api needs everywhere in its codebase and would otherwise duplicate per-route:
errors/— theAppErrorhierarchy (BadRequestError,UnauthorizedError,ForbiddenError,PaymentRequiredError,NotFoundError,RateLimitError,ServiceUnavailableError,SourceError,TimeoutError, ...). One error thrown anywhere in a route handler is caught once, centrally, and turned into a consistent HTTP response — seehandleGlobalErrorbelow.fetch/—HttpClient, the shared HTTP client with retry/timeout behavior, used by everyUpstreamHandlersubclass to call third parties.logger/— structured logging built onAsyncLocalStorage, sogetLogger()returns the request-scoped logger from anywhere without threading aloggerargument through every function call.
Request flow: /v1
Global middleware (mounted once, in apps/api/src/app.ts, in this order) runs on every request regardless of surface:
contextStorage()(hono/context-storage) — makes the active request's context reachable viagetContext()/tryGetContext()from code with noctxparameter (e.g.UpstreamHandler). Must wrap everything else, so it's mounted first.sentry(app)— only ifSENTRY_DSNis set.cors()requestId()(hono/request-id) — generates the per-request id everything else correlates on.mountEnv— validates env once per process (apps/api/src/configs/index.ts) and puts it on the context.Logger(apps/api/src/middlewares/createLogger.middleware.ts) — creates a request-scoped logger, logshttp_request_started, and wraps the rest of the chain inrunWithLoggersogetLogger()works downstream. Logs the terminalhttp_request_completedevent on the way out (or on a thrown error).metricsMiddleware(apps/api/src/middlewares/metrics.middleware.ts) — records Prometheus/OTel HTTP metrics and fires the ClickHouseapi_requestsinsert. See observability.md.trimTrailingSlash(),prettyJSON()
Then the request reaches whichever router matched. For /v1 and /v2 specifically (apps/api/src/routes/index.ts):
apiKeyAuth(apps/api/src/middlewares/apiKeyAuth.middleware.ts) — resolves the caller fromx-api-keyand setsapiCalleron the context.apiKeyRateLimit(apps/api/src/middlewares/rateLimit.middleware.ts) — mounted beforecreditMeterdeliberately: a throttled request must never spend a credit.creditMeter(apps/api/src/middlewares/creditMeter.middleware.ts) — reserves credits for metered callers before the handler runs. See auth-and-credits.md.- The route handler.
Inside a route: route → controller → service → normalizer
Each platform under apps/api/src/routes/<site>/ follows the same file split (see apps/api/src/routes/walmart/ for a concrete example):
<site>.route.ts— the OpenAPI route definition (createRoute): path, method, tags, request/response schemas.<site>.controller.ts— the Hono handler: validates input (via the route's Zod schema), calls the service, wraps the result withok(ctx, data)(apps/api/src/utils/response.utils.ts).<site>.service.ts— the actual upstream call, eitherbuildUrl(...)to the worker or anUpstreamHandlersubclass to a third party.<site>.normalizer.ts— reshapes the upstream's raw response into this API's stable response shape.<site>.schema.ts— Zod request/response schemas shared by the route and controller.
Out to the upstream and back
Third-party calls go through UpstreamHandler (apps/api/src/helpers/upstream/upstream-handler.ts), an abstract base every direct-to-third-party service subclasses. It:
- Records duration/status/bytes for every call, win or lose, via
recordUpstreamInsight(apps/api/src/observability/upstreamInsight.ts) — stashed on the request context sometricsMiddlewarecan attach it to theapi_requestsrow without threading it through every function signature. - Translates failures into the public vocabulary: a transport failure (timeout/connection refused, no HTTP status at all) or a retryable HTTP status becomes
ServiceUnavailableError(503); a non-retryable failure becomesSourceError(500). The specific upstream status/provider code is logged for diagnostics but never echoed to the client. - Archives the raw response body (
apps/api/src/ingestion/response-archiver.ts) unless the subclass opts out — see observability.md for what's optional there.
Back out through the response
- Success: the controller calls
ok(ctx, data), which sets theX-Toolzer-Request-Idheader and returns{ data }. - Failure: anything thrown (from validation,
apiKeyAuth,creditMeter, the service, or the handler) is caught once byhandleGlobalError(apps/api/src/helpers/globalErrorHandler.ts), which normalizesAppErrorsubclasses into{ error: { code, status, message, retryable, details } }viafail(ctx, ...)— same request-id header, same shape, regardless of where in the chain it was thrown. - Zod validation failures on the request itself are caught earlier by
handleZodValidationErrors(apps/api/src/helpers/zodError.ts, wired asdefaultHookinapps/api/src/utils/createHonoInstance.utils.ts) and thrown asBadRequestErrorbefore the handler ever runs.
creditMeter's finally block runs after the handler settles (see auth-and-credits.md for exactly which outcomes commit vs. release), then metricsMiddleware's finally records the request, then Logger logs the terminal event.
Other surfaces, briefly
/huband/appusesessionAuth(apps/api/src/middlewares/sessionAuth.middleware.ts) instead ofapiKeyAuth— cookie or bearer session token, resolved againstplatform.sessions./hubadditionally requiresrequireCapability("api"); most of/apprequiresrequireCapability("rankings")(apps/api/src/middlewares/requireCapability.middleware.ts)./adminapplies its ownadminAuthinside the router rather than at the mount point, so the requirement travels with the routes./internalkeeps public Apify Store GET catalogue views separate fromadminAuth-protected POST controls. The route module owns that split so alternate mounts preserve it.
OpenAPI generation
GET /openapi.json (apps/api/src/app.ts) builds the spec from every mounted route's createRoute schema, then strips operations tagged admin, internal, vantage*, hub, or disapproved* (PRIVATE_TAG_PATTERNS) before returning it — so the published spec, and the Fumadocs site built from it, never lists internal-only operations. GET /llm.txt renders the same spec (OpenAPI 3.1, via @scalar/openapi-to-markdown) as markdown for LLM consumption.