Skip to content

Typed JavaScript API client

Start with the complete song reader HTML. Read the list and choose a song to see its title and cover on the page. The quickstart explains how to save it as index.html and open it over local HTTP. This starter uses https://haneoka.org/embed/api-client.js with no npm installation. Package imports and TypeScript snippets below are for an application that already builds those modules.

@haneoka/api-client/haneoka provides createHaneokaClient for the resource API. The root createApiClient stays a general HTTP client for your own services. Use a matching build with the ./haneoka entry. It works with browser/Node Fetch or a supplied transport(Request).

import { createHaneokaClient } from "@haneoka/api-client/haneoka";
import { ApiClientError } from "@haneoka/api-client";
const api = createHaneokaClient({ baseUrl: "https://haneoka.org/api/v1/" });
const controller = new AbortController();
try {
const servers = await api.servers({ signal: controller.signal });
const song = await api.entity("songs", "100070", {
server: "intl", locale: "en", signal: controller.signal,
});
console.log(servers, song);
} catch (error) {
if (error instanceof ApiClientError) {
console.error(error.code, error.status, error.requestId, error.retryable);
} else throw error;
}
// Cancel a pending call: controller.abort().

baseUrl is the /api/v1/ root. server defaults to intl and comes from servers(); locale accepts ja, en, zh-TW, zh-CN, or ko. Catalog values keep all supplied languages. Authored embed documents can use other locales through embed-core.

let cursor: string | undefined;
do {
const page = await api.page("songs", { server: "intl", limit: 20, ...(cursor ? { cursor } : {}) });
for (const entry of page.items) console.log(entry.id, entry.value);
cursor = page.nextCursor ?? undefined;
} while (cursor !== undefined);
const batch = await api.batch("songs", ["100001", "100070"], { server: "intl" });
console.log(batch.items, batch.missing);

page defaults to limit 50 and validates item IDs, limit, cursor and snapshot identity. Short or empty pages can still continue. Batch accepts 1–100 submitted IDs and validates its envelope. Both can select view using a name declared in the manifest.

index(resource, scope) reads the complete provider index; entity(resource,id,scope) reads one entry; relation(resource,relation,key,scope) reads the manifest-defined relation. Relations do not accept a view. Each supports signal and a decode(value: unknown) callback. The default result is a JSON value; a decoder performs runtime validation and gives your business DTO type.

const song = await api.entity("songs", "100070", {
decode(value) {
if (!value || typeof value !== "object" || !("musicId" in value)) {
throw new TypeError("Expected a song");
}
return value;
},
});

Pass release only for a recorded immutable catalog ID. The client switches to /servers/{server}/... automatically. Ordinary direct aliases reject a release query. chartImageUrl() constructs a current-data image address; it does not fetch or decode binary content:

const url = api.chartImageUrl("100070", "expert", {
server: "intl", locale: "en", format: "svg", height: 720, download: false,
});

ApiClientError preserves method, URL, status, code, optional requestId, details and retryability. Aborted requests use request_aborted and are not retryable. HTTP gateway failures retain their status even with an HTML or malformed error body. Successful invalid JSON/schema responses report invalid_content_type, invalid_json or invalid_payload. The client does not retry automatically.

Supply headers at construction or a custom transport(request) for your host. Ensure any authentication headers reach only their intended origin. Public catalog reads need no session. Use raw response() on the general client when you need image bytes or endpoint-specific error shapes such as game records.

Choose an operation, edit the JSON, and press Load and run. Each preset calls the actual client method. Page requests retain nextCursor in the editor for the next click. client contains constructor options; scope contains catalog call options; request contains general HTTP options. validateMusicId: true enables the shown example decoder. The read demo limits general requests to GET/HEAD and logs request URL/status without logging header values.

Edit the JSON parameters, then load. Modules and requests start only after a click; results include the actual response and transport log.

Language

Preparing example…

View the example source

The example dynamically imports the published https://haneoka.org/embed/api-client.js only when you run it. The manifest identifies the current immutable release and its module paths. SDK files are deployment outputs from committed source. npm publication remains a separate distribution step.

createHaneokaClient(options = {}) supplies Haneoka resource methods. createApiClient(options) supplies general HTTP methods. Both accept these public constructor fields:

FieldType and requirementDefaultMeaning, constraints and errors
baseUrlstring; required for general client, optional for Haneoka clientHaneoka: https://haneoka.org/api/v1/API directory. The general client trims it and adds a trailing slash; an empty string throws TypeError. A malformed base/URL rejects when URL construction or Request creation occurs. Use an absolute HTTP(S) URL in browser/SSR examples.
headersoptional HeadersInit: Headers, record, or pairsno added headersShared request headers. General per-call headers override matching constructor headers, case-insensitively. Native Headers rejects malformed values. Scope methods have no per-call headers field.
transportoptional (request: Request) => Promise<Response>native fetchReceives the complete request, including its signal. Return an actual Response. Transport rejection becomes network_error/request_aborted; throwing ApiClientError preserves it. Customize origin, credentials or deadlines here.

Haneoka’s constructor wraps transport requests with credentials: "omit", mode: "cors" and referrerPolicy: "no-referrer". This covers its registry and resource reads. The general client keeps native Request defaults; the callback can construct a modified Request. Neither constructor has a timeout, retry, byte-budget, cache or dispose option. A caller’s signal/transport controls those policies.

import { createApiClient } from "@haneoka/api-client";
import { createHaneokaClient } from "@haneoka/api-client/haneoka";
const api = createHaneokaClient({
baseUrl: "https://haneoka.org/api/v1/",
headers: { Accept: "application/json" },
transport(request) {
console.log(request.method, request.url);
return fetch(request);
},
});
const general = createApiClient({ baseUrl: "https://haneoka.org/api/v1/" });

The following are complete HaneokaScope/HaneokaReadOptions fields. Pass the fields needed by a call; servers() accepts only signal, and chartImageUrl() has its separate option set below.

Field/argumentType; defaultConstraints, effect and failure
resourcerequired string positional argumentManifest resource name; lowercase letters/digits/hyphens, 1–64 characters, first/last alphanumeric. Reserved groups game, search, account, admin, announcements, auth, catalog, community, garupa, me, release, releases, servers, sources, ui-marks are rejected. Invalid syntax/group throws TypeError; a valid but missing resource returns HTTP404.
id, relation key, image songIdrequired string where the method lists it1–256 characters, first alphanumeric; remaining characters may also include ., _, :, ~, -. The client encodes a segment; / is excluded. Syntax failure throws TypeError; absence is endpoint-specific404.
serveroptional string; intlActive registry slug; lowercase alphanumeric/hyphen, 1–40 characters, first/last alphanumeric. TypeError for syntax,404 for an inactive server. Selects data independently from locale.
localeoptional "ja" | "en" | "zh-TW" | "zh-CN" | "ko"; omittedAdds locale query, retaining localized catalog fields. Invalid locale throws TypeError. Omission lets the endpoint negotiate its supported language. Author-created embed documents can use other languages.
releaseoptional string; current dataReturned r- plus20 lowercase hex digits. Changes resource URL to /servers/{server}/…. Invalid syntax throws TypeError; unavailable snapshot404. Direct HTTP aliases reject a release query.
signaloptional AbortSignal; noneCancels the request. Aborted transport becomes ApiClientError(request_aborted,status0,retryablefalse). Give each operation a fresh controller after cancellation.
viewoptional string; canonical resourceManifest view name with the same syntax as resource. Adds /views/{view} for index/entity/batch/page. Invalid name TypeError, missing view404. Relation rejects any supplied view.
decodeoptional (value: unknown) => T; JSON-value decoderSynchronous runtime validation/conversion. Applied to the complete index/entity/relation, each batch entity or each page item. Throwing causes invalid_payload; a thrown ApiClientError is retained. The generic type alone performs no validation.
MethodArguments/optionsResult and behavior
servers(scope = {})only { signal? }Promise of {id,displayName,region}[], validated from the registry’s releases array. id is the slug, not a release identifier.
index(resource, options = {})shared read optionsPromise of complete resource/view index, or your decoded value. No automatic pagination.
entity(resource, id, options = {})required ID and shared read optionsPromise of one canonical/view entity.
batch(resource, ids, options = {})required readonly string array and shared read options1–100 submitted IDs, counting duplicates before server deduplication; empty/over100/invalid ID throws TypeError. Returns {items: Record<id,T>, missing: string[]}. Missing entries are successful batch results.
page(resource, options = {})shared read options plus limit?, cursor?Returns the envelope below. Requires collection/view entity storage at the endpoint.
relation(resource, relation, key, options = {})relation name has resource-name syntax; key has ID syntaxReturns decoded manifest-defined value. View+relation throws TypeError. Existing empty key result may be {}; missing relation404.
chartImageUrl(songId, difficulty, options = {})separate image options belowReturns a URL synchronously. It makes no request and has no cancellation/decoder.

limit?: number defaults to50 and must be an integer1–100; invalid values throw TypeError before fetching. cursor?: string defaults to no cursor. Return a nonempty cursor unchanged; malformed or mismatched values are rejected by the server. A cursor is bound to server/resource/view and the first release. Keep scope consistent, and restart the first page if that retained release becomes unavailable.

Response fieldTypeUse
itemsreadonly {id:string,value:T}[]Up to the requested limit. Eight-shard scan budgets can produce a short or empty page with more data ahead.
totalnonnegative safe integerManifest count; not this page’s length.
limitinteger1–100Requested effective limit. Client checks it matches the request.
nextCursornonempty string or nullOnly null means completion.
release.schemaliteral haneoka-resource-release-identity-v1Snapshot envelope version.
release.serverstringMust match the requested/default server.
release.releaseIdstringMust match explicit release when supplied.
release.sourceIdstringSource snapshot ID. Preserve it rather than constructing it.

The client validates IDs/envelope/snapshot shape. It does not reorder results, auto-follow pages or combine batch and pagination. The method interfaces keep those two forms separate.

FieldType/defaultRange and meaning
difficultyrequired path argumenteasy, normal, hard, expert, special, master; invalid choice TypeError. Endpoint also requires that difficulty for the song.
serveroptional string; intlSame server validation as scope.
localeoptional supported Haneoka localeCaption language query.
formatoptional svg/png; svgInvalid value TypeError.
heightoptional integer; URL omits it and endpoint defaults720360–1440 panel pixels; invalid value TypeError. Final canvas dimensions come from response headers.
downloadoptional boolean; omittedtrue sends1, false sends0. Omission leaves the endpoint’s attachment default.

Image options have no release/view/decode/signal. Fetch the URL separately for bytes; see chart images for rendering errors and deployment/font requirements.

FieldType/defaultMeaning and constraints
pathrequired string argumentRelative path resolved beneath baseUrl after stripping leading slashes; an absolute HTTP(S) URL keeps its origin. A malformed URL/Request throws the native error.
methodoptional DELETE/GET/HEAD/PATCH/POST/PUT; GETrequest/response option. get forces GET even if JavaScript passes a method. Native Request rejects illegal body/method combinations.
queryoptional readonly recordValues are string/number/boolean/null/undefined or readonly arrays of them. Keys are appended in sorted order; arrays repeat the key. null/undefined entries are omitted. Existing URL query remains and new values are appended.
headersoptional HeadersInit; constructor headersPer-call values override matching shared headers.
bodyoptional BodyInit or null; omittedRequest body. Encode JSON yourself and set its content type; streams/form data follow native Fetch rules.
signaloptional AbortSignal; noneCaller cancellation, including a deadline signal such as AbortSignal.timeout.
decodeoptional (unknown)=>T; no conversionAvailable for get/request. Receives parsed JSON or undefined for HEAD/204/205; decoder errors become invalid_payload. response excludes this option.

get(path, options) parses and validates a successful JSON response. request(path, options) adds a selectable method. Both reject non-2xx as ApiClientError, require application/json or a +json MIME on a body response, and report malformed JSON separately. response(path, options) returns the actual Response, including non-success statuses; inspect ok/status and consume its body yourself. url(path, query) builds a string with no request.

const request = new AbortController();
const dto = await general.request("songs/100070", {
method: "GET", query: { server: "intl", locale: "en" },
headers: { Accept: "application/json" }, signal: request.signal,
});
const raw = await general.response("songs/100070", { method: "HEAD" });
console.log(raw.status, raw.headers.get("X-Haneoka-Release-Id"));
console.log(general.url("songs", { id: ["100001", "100070"] }));

ApiClientError extends Error. Its public fields are name, message, inherited cause, code, details, method, url, status, optional requestId, and retryable. HTTP errors use the returned nested error.code/message/details when available; HTML/bad error JSON falls back to http_STATUS while retaining status. Request ID comes from X-Request-Id. Network failure uses status0, network_error and retryabletrue; cancellation uses request_aborted/retryablefalse. HTTP408/429/5xx are marked retryable. Successful response format/decode failures use invalid_content_type/invalid_json/invalid_payload and are not automatically retried.

The class can be constructed with {code,message,method,url} required, plus cause?, details?, requestId?, retryable? (false) and status? (0), for a custom transport’s structured failure. Methods have no subscribe/events/dispose interface: results arrive through Promise/Response, and the caller owns AbortControllers and response streams.

The package ./browser source entry combines createApiClient, ApiClientError and createHaneokaClient; it adds no separate options or browser-only side effects. Hosted facade exports are selected by the release manifest.

Stable modules use https://haneoka.org/embed/{name}.js; names include api-client, core, vega, cassiopeia, home-spot and vega-theme. To pin a version, read the release manifest and resolve its module paths against https://haneoka.org/embed/. Use paths from that same response for a matching release. Older immutable URLs retain their corresponding behavior.

import { createHaneokaClient } from "https://haneoka.org/embed/api-client.js";
const api = createHaneokaClient();
console.log(await api.entity("songs", "100070", { server: "intl", locale: "en" }));

The manifest identifies the selected production release. Its immutable paths stay fixed as the stable aliases adopt later releases.