Use FERS from JavaScript
The FERS solver is published as a WebAssembly build of the same Rust engine that powers the web app. It runs in the browser or in Node, solves without a server round-trip, and needs no API key on the free tier.
Install
Two packages are published from the same engine and share an identical API — only the installation and bundler setup differ. Pick -web for anything that goes through a bundler, and the unsuffixed package for server-side Node.
# Browser apps (Vite, Next.js, webpack):
npm install @ferscloud/fers-calculation-web
# Node.js / server-side:
npm install @ferscloud/fers-calculationBoth packages are versioned from the engine and move together. Pin them to the same exact version rather than a caret range.
Bundler setup
@ferscloud/fers-calculation-web is a WASM ES module that initialises via top-level await. Most bundlers need a one-time configuration change to allow that; the Node package needs none.
Vite
Add the plugin, then set a build target that supports top-level await.
npm i -D vite-plugin-wasmVite config
// vite.config.ts
import wasm from "vite-plugin-wasm";
// The package initialises with a top-level await, so the build target
// has to be one that supports it. esnext needs no second plugin.
export default {
plugins: [wasm()],
build: { target: "esnext" },
};Next.js and webpack
Transpile the package and turn on async WebAssembly. asyncFunction is required because the module initialises with a top-level await.
// next.config.js
module.exports = {
transpilePackages: ["@ferscloud/fers-calculation-web"],
webpack(config) {
config.experiments = { ...config.experiments, asyncWebAssembly: true };
config.output.environment = {
...config.output.environment,
asyncFunction: true,
};
return config;
},
};The solver is synchronous and CPU-bound. For large models, run it inside a Web Worker so the UI thread stays responsive.
Solve a model
No init() call and no API key. The free tier covers any model up to 100 members.
import { calculate_from_json } from "@ferscloud/fers-calculation";
// No init() call needed — the nodejs and bundler builds initialise the
// WASM module automatically on import.
const res = JSON.parse(calculate_from_json(JSON.stringify(myModel)));
if (res.ok) {
const data = res.result; // displacements, member_results, unity_checks, …
} else {
console.error(res.error.code, res.error.message);
}The response envelope
Every solver call returns a JSON envelope, so you parse once and branch on ok — you never have to sniff whether the returned string looks like an error. The return value is always valid JSON.
// success
{ "ok": true, "result": { /* the full result document */ } }
// failure (invalid model, over the member limit, malformed JSON, …)
{ "ok": false, "error": { "code": "LimitExceeded",
"message": "Number of members (250) exceeds allowed maximum of 100" } }| error.code | Means |
|---|---|
InvalidJson | The model string was not parseable JSON. |
LimitExceeded | More members than the active tier allows (100 free, 10,000 Pro). |
SolveError | The model parsed but could not be solved — most often a singular stiffness matrix from a missing or under-specified support. |
InternalPanic | A bug in the engine. Please report it. |
InternalSerialization | The results could not be serialised. Also a bug worth reporting. |
TypeScript types
The browser package ships generated model types alongside the function signatures. They are generated from the engine's OpenAPI schema, so they track the published version rather than being maintained by hand.
import type {
FERS,
ResultsBundle,
} from "@ferscloud/fers-calculation-web/fers-models";
// FERS — the input model
// ResultsBundle — the `result` payload of a successful envelope
// Both are generated from the engine's OpenAPI schema, so they track
// the published version.Crediting FERS on the free tier
Free-tier results carry an attribution object minted inside the solver. Pro results, solved with a valid token, do not — so the same code shows a credit on free and white-labels on Pro, with no flag to set.
getFersAttribution, fersAttributionText and createFersBadge are exported alongside it. All of them accept the envelope, the parsed model or the results bundle, and all return nothing for a Pro result or an error envelope.
import { fersAttributionHtml } from "@ferscloud/fers-calculation-web/badge.js";
// Free-tier results carry result.attribution; this renders the credit.
// With a Pro solve token the field is absent and this returns "" — the
// same code white-labels, with no flag to set.
const credit = fersAttributionHtml(res);If you ship the free tier in an application, please display the credit. It is the only thing the free tier asks of you. Available from engine 0.2.61.
Pro limits with a solve token
Pro limits are unlocked by passing a short-lived signed token issued by the FERS Cloud server. The token is verified inside the WebAssembly module against an Ed25519 public key baked into the binary, so it cannot be forged.
Your server holds the API key and exchanges it for a 30-minute token; the browser only ever sees the token. Keep FERS_API_KEY server-side — never in browser code.
// pages/api/solve-token.ts
const FERS_API_KEY = process.env.FERS_API_KEY!;
let cached: { token: string; expiresAt: number } | null = null;
export default async function handler(req, res) {
const now = Date.now();
// Re-use if more than 5 minutes remain
if (cached && cached.expiresAt - now > 5 * 60 * 1000) {
return res.json({ token: cached.token });
}
const resp = await fetch("https://ferscloud.com/api/solver/token", {
method: "POST",
headers: { "X-API-Key": FERS_API_KEY },
});
if (!resp.ok) return res.status(502).json({ error: "Token fetch failed" });
const { token, expiresAt } = await resp.json();
cached = { token, expiresAt: new Date(expiresAt).getTime() };
return res.json({ token });
}Calling the solver with a token
If the token is missing, expired or invalid, the solver falls back to the free member limit — it never throws. A genuine solve failure still comes back as { ok: false, error } rather than an exception.
import { calculate_from_json_with_token } from "@ferscloud/fers-calculation";
const { token } = await fetch("/api/solve-token").then((r) => r.json());
const res = JSON.parse(
calculate_from_json_with_token(JSON.stringify(myModel), token),
);
if (!res.ok) throw new Error(`${res.error.code}: ${res.error.message}`);
const data = res.result;| Free | Pro | |
|---|---|---|
| Max members | 100 | 10,000 |
| Function | calculate_from_json | calculate_from_json_with_token |
| Requires a token | No | Yes |
| Token lifetime | — | 30 minutes |
Deflected shape
Set include_member_deflected_shape in the model's analysis options to get a ready-to-plot, load-exact deflected shape per member — the member's global displacement sampled along its length — instead of reconstructing the curve yourself.
It is off by default to keep the payload lean, and omitted from member_results when not requested.
const model = {
/* … model + load cases … */
analysis: {
/* … */
options: {
/* … */
include_member_deflected_shape: true,
},
},
};
const res = JSON.parse(calculate_from_json(JSON.stringify(model)));
const mr = res.result.results.loadcases["…"].member_results["1"];
// mr.member_displacements: [{ x_frac, displacement: [dx, dy, dz] }, …]
// x_frac 0–1 along the member; displacement in the global input frame.Other ways in
- Why solve structures in JavaScript — the case for running the solver client-side.
- REST API — call the solver over HTTP from any language.
- MCP server — let Claude, ChatGPT, Cursor or VS Code drive FERS directly.
- Python package — the same engine, from a script.
Related pages
See also
Frequently asked questions
Do I need an API key to solve in the browser?
calculate_from_json works with no key and no account for models up to 100 members. A key is only needed to mint the Pro solve token.Does the model leave the browser?
Why does Vite need extra configuration?
await. vite-plugin-wasm handles the WebAssembly import, and build.target: "esnext" keeps that await in the output — which needs Chrome, Edge or Firefox 89+, or Safari 15+. webpack needs asyncWebAssembly and asyncFunction in its experiments and output settings. If vite-plugin-top-level-await is still in your config from an earlier version of this page, you can drop it: recent @swc/core releases make it fail at vite build.Should I run the solver in a Web Worker?
Do I need an init() call?
await init(); that is no longer required.What happens if the solve token has expired?
LimitExceeded.Which package do I use on the server?
@ferscloud/fers-calculation, built for the Node target. It needs no bundler configuration and self-initialises on import.