npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 name
  • function: 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-data encoding for plain objects, FormData, and file Blobs.
  • Optional per-client / per-service / per-request overrides (headers, endpoint, fetch, signal, etc).

Install

npm i @sfutureapps/api-sdk

This 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:

  • baseUrl is typically your API origin (example: https://api.yourdomain.com).
  • endpoint defaults to /v3/api/exec and is appended as baseUrl + endpoint.
    • Tip: avoid a trailing / on baseUrl if your endpoint starts with /.

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

  • baseUrl is the API origin, e.g. https://api.yourdomain.com
  • endpoint is appended to baseUrl (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 name
  • x-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:

  • string values are sent as-is.
  • Blob values are appended as files.
  • other non-null values are JSON.stringify-ed.
  • null/undefined values 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?: HeadersInit
  • endpoint?: string (default /v3/api/exec)
  • credentials?: RequestCredentials
  • signal?: AbortSignal
  • fetch?: 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 sync

In 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.com
  • defaultOptions?: 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.