Polkadot-API (PAPI): A Modern TypeScript API for Polkadot Builders
This expert guide explains what Polkadot-API (PAPI) is, why it matters, and how to use it to build fast, type-safe dApps across Polkadot chains. We compare PAPI with @polkadot/api
(polkadot.js) and Dedot, then walk through setup, code examples, governance & ink! integrations, common pitfalls, and SEO-ready FAQs.
Why PAPI? (Problem → Benefit)
Problem: Many Polkadot dApps still rely on heavy RPC clients, manual codec handling, and loosely typed calls. This leads to brittle code, slower bundles, and runtime breakage during upgrades.
Benefit with PAPI: PAPI is light-client-first, ships fully typed APIs generated from on-chain metadata, and favors small, tree-shakable bundles—while supporting observables or promises, multi-chain connections, and modern developer experience (DX).
ELI5
PAPI is a TypeScript toolkit that talks to Polkadot chains. It learns types from the chain itself, so your IDE autocompletes pallets/calls/events; it can run a light client in the browser, and it helps keep your app small and fast.
How PAPI Works
At its core, PAPI consists of:
- A client that connects via a provider (WebSocket or light client).
- A Typed API generated by a small CLI that downloads each chain’s metadata and produces type descriptors you import.
- Promise and Observable interfaces for storage, constants, runtime calls, transactions, and events—plus dynamic imports and native
bigint
to keep the bundle lean.
PAPI is designed around modern JSON-RPC and light-client workflows (so you can run trust-minimized dApps in the browser).
Key Features
- Light-client first: Run trust-minimized apps in web & Node environments.
- Fully typed DX: Types/docs generated from on-chain metadata; IDE autocompletes pallets, calls, events.
- Multi-chain by design: Connect to multiple chains in one app.
- Small & fast: Tree-shakable subpaths, native
bigint
, dynamic imports, minimal bundle impact. - Promises & Observables: Subscribe to blocks and transactions or use simple promises.
- Upgrade aware: Regenerate descriptors per runtime change and preflight compatibility.
- First-class transactions: Typed extrinsics, fee estimation, error types, and watchable lifecycle.
PAPI vs polkadot.js vs Dedot (Comparison)
Below is a practical view of where each library shines for 2025 builds.
Tool | Type System & DX | Light Client Support | Bundle Footprint | Codegen / Types From Metadata | JSON-RPC | Multi-chain | Promises/Observables | ink! / Gov SDKs | Notes |
---|---|---|---|---|---|---|---|---|---|
PAPI | Fully typed descriptors; IDE autocompletion; native bigint | Designed light-client-first | Small / tree-shakable | CLI-generated descriptors | Modern spec | Yes | Both | SDKs for ink! & OpenGov | Modern, modular, upgrade-aware |
polkadot.js (@polkadot/api ) | Mature API; strong community | RPC-centric (light client separate) | Heavier in typical builds | Dynamic from metadata at runtime | Legacy-compatible | Yes | Promise & RxJS | Broad ecosystem adoption | The long-standing standard |
Dedot | Lightweight; tree-shakable | Works with light clients | Small / tree-shakable | Per-chain TS types | Multi-version support | Yes | Promise-style | ink! support; migration-friendly | Next-gen client emphasizing DX & size |
Takeaway: For new TypeScript dApps that want light-client, strict typing, and small bundles, PAPI (or Dedot) is often the best fit; @polkadot/api
remains a solid, mature baseline with broad ecosystem adoption.
Quick Start: Step-by-Step
1) Install & generate types
npm i polkadot-apinpx papi add dot -n polkadotnpx papi# (Optional) add "postinstall": "papi" to package.json
This downloads the latest Polkadot metadata and generates the @polkadot-api/descriptors
you’ll import.
2) Create a client (light client in the browser)
import { dot } from "@polkadot-api/descriptors";import { createClient } from "polkadot-api";import { getSmProvider } from "polkadot-api/sm-provider";import { chainSpec } from "polkadot-api/chains/polkadot";import { startFromWorker } from "polkadot-api/smoldot/from-worker";import SmWorker from "polkadot-api/smoldot/worker?worker";
const worker = new SmWorker();const smoldot = startFromWorker(worker);const chain = await smoldot.addChain({ chainSpec });
const client = createClient(getSmProvider(chain));const dotApi = client.getTypedApi(dot);
3) Query storage (strongly typed)
const accountInfo = await dotApi.query.System.Account.getValue("14...SS58");
4) Add a signer (extension or custom)
Use a browser wallet via the injected signer:
import { getInjectedExtensions, connectInjectedExtension } from "polkadot-api/pjs-signer";
const [extId] = getInjectedExtensions();const ext = await connectInjectedExtension(extId);const { polkadotSigner } = ext.getAccounts()[0];
Or construct a signer from your own key material (sr25519/ed25519/ecdsa).
Practical Example: Transfer + Fee Estimation + Watch
1) Build the extrinsic (typed)
import { MultiAddress } from "@polkadot-api/descriptors";
const tx = dotApi.tx.Balances.transfer_keep_alive({ dest: MultiAddress.Id("16...dest"), value: 10n ** 10n, // 1 DOT});
2) Estimate fees first
const estimated = await tx.getEstimatedFees("16...sender");console.log("Estimated fee (planck):", estimated);
3) Sign & submit and watch lifecycle
// Promise flavor: resolves at finalizationconst result = await tx.signAndSubmit(polkadotSigner);console.log(result.block.number, result.ok, result.events);
// Observable flavor: granular events while it propagatestx.signSubmitAndWatch(polkadotSigner).subscribe(ev => { if (ev.type === "signed") console.log("Signed:", ev.txHash); if (ev.type === "broadcasted") console.log("Broadcasted:", ev.txHash); if (ev.type === "txBestBlocksState" && ev.found) console.log("In best block…"); if (ev.type === "finalized") console.log("Finalized in", ev.block.number);});
Governance & ink! SDKs
- OpenGov: The Governance SDK wraps track curves, preimages, spender origins, and subscription helpers. It can auto-select tracks, compute confirmation windows, and expose chart-ready thresholds—ideal for dashboards.
- ink!: The ink! SDK provides type-safe deploy, dry-run, gas/storage estimates, message calls, and event decoding for ink! v6 contracts—purpose-built for TypeScript UIs.
Common Pitfalls & Pro Tips
- Forgetting codegen: After upgrading PAPI or when a chain updates runtime metadata, regenerate descriptors (
papi
)—or wire it topostinstall
. bigint
handling: Amounts arebigint
. Don’t mix with JSnumber
; prefer constants like10n ** 10n
.- Signer mismatches: Use the correct scheme (sr25519/ed25519/ecdsa) and address format. The signer helpers clarify this.
- Watching tx state: Prefer
signSubmitAndWatch
for robust UX; handle invalidity via typed errors and surfacedispatchError
to users. - Multi-chain coordination: Keep per-chain descriptors separate and share client utilities (observables) thoughtfully to avoid mixing state.
- RPC vs light client: For decentralization and resilience, prefer the light client; you can still use WebSocket providers where needed.
FAQs
1) How is PAPI different from @polkadot/api
?
PAPI emphasizes light-client-first architecture, code-generated types, native bigint
, and modern DX; @polkadot/api
is the long-standing, RPC-centric standard with Promise/RxJS APIs and broad ecosystem support.
2) Do I need a light client to use PAPI? No. PAPI supports WebSocket providers, but it shines with a light client in browser or Node.
3) Will runtime upgrades break my app? Regenerate descriptors and use compatibility checks; PAPI helps you preflight upgrades and keep types aligned.
4) Can I connect to multiple chains in one app? Yes—multi-chain is supported out of the box.
5) How do I integrate wallets?
Use polkadot-api/pjs-signer
to connect to injected browser extensions and obtain a PolkadotSigner
.
6) Does PAPI support ink! smart contracts? Yes—use the ink! SDK for type-safe deployment, calls, events, and estimates.
7) Can I estimate fees before sending a transaction?
Yes—getEstimatedFees(from, options?)
returns a bigint
fee estimate.
8) Is Dedot a competitor or complement? Dedot is another modern TS client focused on small bundles and DX; both Dedot and PAPI improve the developer experience over older patterns.
Conclusion
If you’re starting a new Polkadot dApp in 2025, make PAPI your default. You’ll ship smaller bundles, gain compile-time safety, and get a first-class light-client UX—without sacrificing observables, fee estimation, or governance/ink! workflows.