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-calculation

Both 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-wasm

Vite 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.codeMeans
InvalidJsonThe model string was not parseable JSON.
LimitExceededMore members than the active tier allows (100 free, 10,000 Pro).
SolveErrorThe model parsed but could not be solved — most often a singular stiffness matrix from a missing or under-specified support.
InternalPanicA bug in the engine. Please report it.
InternalSerializationThe 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;
FreePro
Max members10010,000
Functioncalculate_from_jsoncalculate_from_json_with_token
Requires a tokenNoYes
Token lifetime30 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

Related pages

See also

Frequently asked questions

Do I need an API key to solve in the browser?

No. 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?

No. The solver is compiled to WebAssembly and runs on the client, so the model and the results stay on the user's machine. Nothing is sent to FERS Cloud.

Why does Vite need extra configuration?

The browser package is a WASM ES module that initialises with a top-level 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?

For anything beyond a handful of members, yes. The solver is synchronous and CPU-bound, so calling it on the UI thread will block rendering for the duration of the solve.

Do I need an init() call?

No. Both the Node and bundler builds initialise the WebAssembly module automatically on import. Older documentation showed await init(); that is no longer required.

What happens if the solve token has expired?

The solver silently falls back to free-tier limits. It never throws on an invalid token, so a model within the free member limit keeps solving and a larger one returns 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.

Are the results the same as the web app's?

Yes — it is the same Rust engine, compiled to a different target. The web app itself runs this WebAssembly build.