@sfutureapps/api-sdk
v3.0.35
Published
A tiny JavaScript/TypeScript client for calling a ThinkPHP-style “gateway exec” endpoint.
Readme
@sfutureapps/api-sdk
A tiny JavaScript/TypeScript client for calling a ThinkPHP-style “gateway exec” endpoint.
It’s designed for backends that expose one HTTP endpoint and route calls by a class + function pair (sometimes used by ThinkPHP “gateway” implementations).
It posts multipart/form-data to a single endpoint (default: /v3/api/exec) with:
class: service namefunction: method name- additional fields from your payload
The gateway is expected to return JSON like:
{ "code": 1, "msg": "ok", "data": {} }If code !== 1, the client throws an Error(msg).
What you get
- A minimal client with one method:
call(service). call(service)returns a Proxy where any property access becomes an async function.- Automatic
multipart/form-dataencoding for plain objects,FormData, and fileBlobs. - Optional per-client / per-service / per-request overrides (
headers,endpoint,fetch,signal, etc).
Install
npm i @sfutureapps/api-sdkThis package is ESM ("type": "module"). Use ESM imports in Node.js and modern bundlers.
Quick start
import { createClient } from "@sfutureapps/api-sdk";
const client = createClient("https://api.yourdomain.com");
// Create a service proxy
const user = client.call("User");
// Call remote method: POST { class: 'User', function: 'profile', ...payload }
const profile = await user.profile({ id: 123 });
console.log(profile);API
import type { GatewayRequestOptions, GatewayServiceProxy } from "@sfutureapps/api-sdk";
function createClient(
baseUrl: string,
defaultOptions?: GatewayRequestOptions
): {
call<TResult = unknown, TPayload = unknown>(service: string, serviceOptions?: GatewayRequestOptions): GatewayServiceProxy<TResult, TPayload>;
};Notes:
baseUrlis typically your API origin (example:https://api.yourdomain.com).endpointdefaults to/v3/api/execand is appended asbaseUrl + endpoint.- Tip: avoid a trailing
/onbaseUrlif yourendpointstarts with/.
- Tip: avoid a trailing
Service + method mapping
client.call('SomeService') returns a Proxy. Any property you access becomes a callable async function.
Example:
const order = client.call("Order");
// -> gateway function = 'create'
const created = await order.create({ sku: "ABC", qty: 2 });
// -> gateway function = 'detail'
const detail = await order.detail({ id: created.id });Authentication / headers
You can provide default headers when constructing the client, and/or override per-service and per-request.
import { createClient } from "@sfutureapps/api-sdk";
const client = createClient("https://api.yourdomain.com", {
headers: {
Authorization: "Bearer YOUR_TOKEN",
},
});
const user = client.call("User");
await user.profile({ id: 123 });Per request override:
const user = client.call("User");
await user.profile(
{ id: 123 },
{
headers: { Authorization: "Bearer OTHER_TOKEN" },
}
);Full example (Node.js 18+, TypeScript)
This example shows:
- default auth headers
- per-request header override
- aborting a request
- file upload via
Blob(multipart) - basic error handling
import { createClient } from "@sfutureapps/api-sdk";
async function main() {
// 1) Create a client
const client = createClient("https://api.yourdomain.com", {
// Optional: override the gateway endpoint if yours differs
endpoint: "/v3/api/exec",
headers: {
Authorization: `Bearer ${process.env.API_TOKEN ?? ""}`,
},
// credentials: 'include', // if you rely on cookies in browsers
});
// 2) Create service proxies
const user = client.call("User");
const upload = client.call("Upload");
// 3) Normal call
const profile = await user.profile({ id: 123 });
console.log("profile:", profile);
// 4) Per-request override (e.g. use a different token once)
const profileAsOther = await user.profile({ id: 123 }, { headers: { Authorization: "Bearer OTHER_TOKEN" } });
console.log("profile (other token):", profileAsOther);
// 5) Abort example
const controller = new AbortController();
const slowPromise = user.slowOperation({ ms: 5000 }, { signal: controller.signal });
controller.abort();
try {
await slowPromise;
} catch (err) {
console.log("aborted:", String(err));
}
// 6) Upload a file (Node 18+ has Blob/FormData built in)
const file = new Blob([Buffer.from("hello world\n")], { type: "text/plain" });
const uploaded = await upload.put({ folder: "docs", file });
console.log("uploaded:", uploaded);
}
main().catch((err) => {
// Gateway errors are thrown as Error(msg) when code !== 1
console.error("request failed:", err);
process.exitCode = 1;
});Endpoint and baseUrl
baseUrlis the API origin, e.g.https://api.yourdomain.comendpointis appended tobaseUrl(default:/v3/api/exec)
const client = createClient("https://api.yourdomain.com", {
endpoint: "/v3/api/exec",
});
// Or override per call:
const svc = client.call("User", { endpoint: "/v3/api/exec" });URL query params
You can append URL query params to the gateway endpoint via query:
const user = client.call("User");
// POST https://api.yourdomain.com/v3/api/exec?tenant=acme&v=2
await user.profile({ id: 123 }, { query: { tenant: "acme", v: 2 } });query can also be set at the client or service level and is shallow-merged:
- client
query - service
query - request
query(wins)
Send class / function via headers or query
By default this SDK sends gateway routing metadata in headers:
x-gateway-class: service namex-gateway-function: method name
You can override this behavior with meta.
const user = client.call("User");
// Sends headers (default):
// x-gateway-class: User
// x-gateway-function: profile
await user.profile({ id: 123 });You can also place the routing metadata into the URL query string:
await user.profile(
{ id: 123 },
{ meta: { placement: "query" } } // adds ?class=User&function=profile
);If your backend expects class + function inside the multipart/form-data payload (legacy gateway style), opt in:
await user.profile(
{ id: 123 },
{ meta: { placement: "form" } } // adds form fields: class, function
);Payload formats
The payload you pass to a method can be:
1) Plain object
For object payloads:
stringvalues are sent as-is.Blobvalues are appended as files.- other non-null values are
JSON.stringify-ed. null/undefinedvalues are skipped.
const file = new Blob(["hello"], { type: "text/plain" });
await client.call("Upload").put({ folder: "docs", file });2) FormData
If you pass a FormData, its entries are appended directly.
const form = new FormData();
form.append("id", "123");
form.append("note", "hi");
await client.call("User").update(form);Request options
Every call accepts an optional GatewayRequestOptions as the second argument.
Supported options:
headers?: HeadersInitendpoint?: string(default/v3/api/exec)credentials?: RequestCredentialssignal?: AbortSignalfetch?: typeof fetch(inject your own fetch)
Example with abort:
const controller = new AbortController();
const promise = client.call("User").profile({ id: 123 }, { signal: controller.signal });
controller.abort();
await promise;Using in Node.js (fetch)
This SDK uses fetch. In Node 18+ it’s built in.
If you’re on Node 16 or older, pass your own fetch implementation:
import { createClient } from "@sfutureapps/api-sdk";
import fetch from "node-fetch";
const client = createClient("https://api.yourdomain.com", { fetch: fetch as any });TypeScript: typed API via generated/api
This package can ship a generated declaration file at generated/api.d.ts.
- The build/publish includes
generated/. - You can sync/refresh it (for the maintainer of the SDK) using:
# Example: pulls a .d.ts from your gateway and writes generated/api.d.ts
GATEWAY_TYPES_URL=https://api.yourdomain.com/types/api.d.ts npm run syncIn your app, import types from @sfutureapps/api-sdk/generated/api and use them to type results/payloads:
import { createClient } from "@sfutureapps/api-sdk";
import type { components } from "@sfutureapps/api-sdk/generated/api";
type UserProfile = components["schemas"]["UserProfile"];
const client = createClient("https://api.yourdomain.com");
const user = client.call<UserProfile, { id: number }>("User");
const profile = await user.profile({ id: 123 });Notes:
call<TResult, TPayload>(service)sets the default result/payload types for methods under that service proxy.- If different methods return different shapes, you can create separate proxies or cast per call.
API reference
createClient(baseUrl, defaultOptions?)
baseUrl: string– e.g.https://api.yourdomain.comdefaultOptions?: GatewayRequestOptions
client.call<TResult = unknown, TPayload = unknown>(service, serviceOptions?)
Returns a proxy object where each property is a method:
(payload?: TPayload, options?: GatewayRequestOptions) => Promise<TResult>
License
Not specified.
