Skip to content

Getting Started

Joël Deffner edited this page Sep 3, 2026 · 2 revisions

Getting Started

Requirements

  • Node 18 or newer.
  • The Steam client, running and logged in. The library talks to the client, not to the web API.
  • Windows x64, Linux x64, Linux ARM64, or macOS (x64 and Apple silicon; the library is one universal binary). Any other process.platform makes the loader throw steamwand: unsupported platform <name>. Valve ships no Windows ARM64 library.
  • A Steamworks partner account if you want to work with your own app id. For learning and testing, app id 480 (Spacewar) is public and needs no account.

There is no compiler step. The only runtime dependency is koffi, and the steam_api redistributables ship inside the package.

Install

pnpm add steamwand.js
npm install steamwand.js

App id

Steam needs to know which app the process is running as, before SteamAPI_InitFlat runs. There are two ways to tell it.

The appId option (recommended for Node). init({ appId: 480 }) sets process.env.SteamAppId and process.env.SteamGameId to '480' and then initializes. Nothing on disk is needed, and the value is visible to anything else in the process.

const steam = init({ appId: 480 });
console.log(steam.appId); // 480

steam_appid.txt. The Steam library also reads a file named steam_appid.txt from the process working directory. It holds the app id as plain text and nothing else. This is the usual arrangement for a shipped game, where the file sits next to the executable. If you use it, omit appId and init falls back to Number(process.env.SteamAppId ?? 0) for the steam.appId property, which is 0 when no environment variable is set. Pass appId anyway if you want steam.appId and steam.workshop to target the right app.

Testing with 480. Spacewar is Valve's public sample app. Any Steam account can initialize under it, create workshop items on it, and delete them again. Use it while you learn the API, then switch to your own app id.

Your first script

Save this as first.ts and run it with npx tsx first.ts, with Steam running.

import { init, SteamInitError } from 'steamwand.js';

async function main() {
  const steam = init({ appId: 480 });

  // `const char *` returns come back as plain strings.
  console.log('user:', steam.friends.GetPersonaName());
  console.log('language:', steam.apps.GetCurrentGameLanguage());

  // 64-bit values are bigint. accountId() is the lower 32 bits, as a number.
  console.log('steam id:', steam.steamId());
  console.log('account id:', steam.accountId());

  // Out parameters are Buffers that you allocate.
  const folder = Buffer.alloc(1024);
  steam.apps.GetAppInstallDir(480, folder, folder.length);
  console.log('install dir:', folder.toString('utf8', 0, folder.indexOf(0)));

  // Async Steam calls resolve through the dispatch pump.
  const page = await steam.workshop.getUserItems(1, steam.accountId());
  console.log('published items:', page.totalResults);
  for (const item of page.items) console.log(' ', item.fileId, item.title);

  steam.close();
}

main().catch((err) => {
  if (err instanceof SteamInitError) console.error('init failed:', err.message);
  else console.error(err);
  process.exit(1);
});

If init throws, the message is Valve's own diagnostic text, not something this library wrote. See Troubleshooting.

Process lifecycle

init does four things: it sets the app id environment variables (only when you pass appId), loads the redistributable, calls SteamAPI_InitFlat, and starts the manual dispatch pump.

The pump is a setInterval that calls SteamAPI_ManualDispatch_RunFrame every pumpIntervalMs milliseconds, 50 by default. Callbacks and call results only arrive while it runs, so nothing async resolves faster than one pump tick. Lower the interval if you want tighter progress reporting:

const steam = init({ appId: 480, pumpIntervalMs: 16 });

The timer is unref'd, so it never keeps an idle process alive. While at least one async Steam call is in flight, the timer is ref'd again, so your process will not exit in the middle of an upload. When the last pending call settles, it goes back to unref'd.

steam.close() stops the pump and calls SteamAPI_Shutdown. It is idempotent, so calling it twice is safe. Any call still in flight rejects with steamwand: dispatch stopped while call was in flight. Because of the unref, a short script that forgets close() still exits; call it anyway, so Steam shuts down in order.

Do not mix this with SteamAPI_RunCallbacks. Manual dispatch owns the whole callback queue for the process, and only one pump may exist. See How-It-Works.

CommonJS and ESM

The package is CommonJS ("type": "commonjs", main: dist/index.js) and ships its own dist/index.d.ts.

// CommonJS
const { init, flat } = require('steamwand.js');
// TypeScript, or ESM through Node's named-export detection
import { init, flat } from 'steamwand.js';

Named imports from a plain .mjs file work through Node's CommonJS named-export detection. If your bundler or runtime does not do that detection, import the default and destructure it:

import pkg from 'steamwand.js';
const { init } = pkg;

Top-level await needs an ESM context. In a CommonJS file, wrap the awaits in an async function as the first script above does.

Shipping an app

There is no build step to reproduce and no prebuilt binary to fetch. npm pack includes dist/ and runtime/, and the loader resolves the library by absolute path relative to the installed package:

Platform File
Windows x64 runtime/win64/steam_api64.dll
Linux x64 runtime/linux64/libsteam_api.so
Linux ARM64 runtime/linuxarm64/libsteam_api.so
macOS (x64 and arm64) runtime/osx/libsteam_api.dylib

Three consequences for packaging. First, if you bundle your app into a single file (esbuild, pkg, an Electron asar), the runtime/ folder must still exist on disk two levels above the emitted dist/runtime/platform.js, or the load fails. Second, when that is inconvenient, point the loader anywhere you like:

const steam = init({ appId: 480, libPath: '/opt/mygame/steam_api64.dll' });

Third, koffi 3 ships its native binary as a separate package, @koromix/koffi-<os>-<arch>, installed as a sibling of koffi in node_modules (npm installs only the one for the build machine). An Electron asarUnpack or files rule that keeps steamwand.js/** alone misses it and fails at runtime with Cannot find the native Koffi module; keep koffi/** and @koromix/** as well.

Shipping the Steamworks redistributables with your game is normal practice and allowed under the Steamworks SDK Access Agreement. The SDK headers and steam_api.json are not, and they are not in this package.

Next: Core-API for the full surface, Workshop for the curated workshop layer, Recipes for worked examples.

Clone this wiki locally