All articles
solanakittypescriptweb3js

@solana/kit: the modern JS client, functional + tree-shakeable

@solana/kit replaces the classic web3.js with a functional, tree-shakeable, strict-typed client. Here's the primitive shape and the migration patterns.

Share
devrels.xyz/a/73short link

@solana/kit (formerly @solana/web3.js v2) is the official successor to the classic @solana/web3.js client. It's not a refresh — it's a ground-up redesign around functional composition, branded types, and tree shaking. A swap-only bundle drops from ~250KB (classic web3.js) to ~25KB (kit).

The trade: more verbose, more types, more explicit. Here's the design as it actually is.

Branded types instead of strings

In classic web3.js, a Solana address was a PublicKey class. In kit, it's a branded primitive — a string with a compile-time tag that distinguishes it from a regular string:

typescript
import type { Address } from "@solana/kit"
import { address } from "@solana/kit"

// Address is a string brand. You can't pass a raw string where an
// Address is expected — TypeScript catches it at compile time.
const usdc: Address = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")

// At runtime, it's a string. No class, no method overhead.
console.log(typeof usdc)  // "string"
console.log(usdc)         // "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"

Same pattern for Signature, Blockhash, Slot, Lamports, etc — each is a compile-time-checked brand on a primitive value.

Functional composition with pipe

typescript
import {
  createSolanaRpc,
  createKeyPairSignerFromBytes,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayer,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstruction,
  signAndSendTransactionMessageWithSigners,
  lamports,
} from "@solana/kit"
import { getTransferSolInstruction } from "@solana-program/system"

const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com")
const signer = await createKeyPairSignerFromBytes(secretBytes)

const ix = getTransferSolInstruction({
  source:      signer,
  destination: recipient,
  amount:      lamports(10_000_000n),
})

const { value: bh } = await rpc.getLatestBlockhash().send()

const tx = pipe(
  createTransactionMessage({ version: 0 }),
  (m) => setTransactionMessageFeePayer(signer.address, m),
  (m) => setTransactionMessageLifetimeUsingBlockhash(bh, m),
  (m) => appendTransactionMessageInstruction(ix, m),
)

const sig = await signAndSendTransactionMessageWithSigners(tx)

Each step is a pure function: takes a message, returns a new message. No mutation, no this, every step independently importable and tree-shakeable.

Codecs for serialization

Kit ships @solana/codecs, a typed binary codec library. Use it to serialize / deserialize account data without hand-rolling Borsh:

typescript
import {
  getStructCodec, getU64Codec, getBoolCodec, getAddressCodec,
} from "@solana/kit"

const VaultCodec = getStructCodec([
  ["owner",      getAddressCodec()],
  ["balance",    getU64Codec()],
  ["is_active",  getBoolCodec()],
])

// Encode an object → Uint8Array
const bytes = VaultCodec.encode({ owner: someAddress, balance: 1000n, is_active: true })

// Decode Uint8Array → typed object
const vault = VaultCodec.decode(accountInfo.data)
console.log(vault.balance)  // bigint

Each codec is composable and bidirectional. Anchor IDLs generate codec definitions for free via codama.

Per-program instruction packages

Where classic web3.js bundled every program's instruction builders inside the main package, kit ships them as separate packages — install only what you use:

sh
npm install @solana/kit                          # core
npm install @solana-program/system               # SystemProgram (transfer, createAccount, …)
npm install @solana-program/token                # SPL Token instructions
npm install @solana-program/token-2022           # Token-2022 instructions
npm install @solana-program/compute-budget       # ComputeBudgetProgram
npm install @solana-program/address-lookup-table # ALT instructions
npm install @solana-program/memo                 # Memo program

Your transfer-only bundle pulls in @solana/kit + @solana-program/system and stops there. ~25KB gzipped.

The rpc object

Every JSON-RPC method maps to a chainable call returning a promise of a typed response:

typescript
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com")

// .send() actually fires the call; everything before is a builder
const slot           = await rpc.getSlot().send()
const balance        = await rpc.getBalance(myAddress).send()
const accountInfo    = await rpc.getAccountInfo(myAddress, { encoding: "base64" }).send()
const programAccts   = await rpc.getProgramAccounts(programId, {
  filters: [{ dataSize: 165 }],
  encoding: "base64",
}).send()

// Subscriptions are a separate transport
import { createSolanaRpcSubscriptions } from "@solana/kit"
const subs = createSolanaRpcSubscriptions("wss://api.mainnet-beta.solana.com")
const notifications = await subs.accountNotifications(myAddress).subscribe({ abortSignal })
for await (const update of notifications) {
  console.log("account changed:", update)
}

Why bigint everywhere

Lamports, slots, supply, balances — all bigint in kit. JavaScript's Number can't safely represent values past 2^53 - 1; Solana's u64 amounts blow past that for large balances. Kit's bigint-everywhere policy prevents the silent truncation that used to plague classic web3.js code.

Migration cheat sheet

text
Classic web3.js              →  @solana/kit
─────────────────────────────────────────────────────────────────
new Connection(url)             createSolanaRpc(url)
new PublicKey(str)              address(str)  (or just the Address brand)
keypair.publicKey               signer.address
LAMPORTS_PER_SOL                lamports(1_000_000_000n)
new Transaction().add(ix)       pipe(createTransactionMessage(…), appendInstruction(ix, …))
conn.sendTransaction(tx, signers) signAndSendTransactionMessageWithSigners(tx)
SystemProgram.transfer(…)       getTransferSolInstruction(…) from @solana-program/system
conn.getAccountInfo(pk)         rpc.getAccountInfo(addr, opts).send()
conn.onAccountChange(pk, cb)    subscribe via createSolanaRpcSubscriptions

References

Kit isn't a drop-in replacement — it's a redesign. The functional shape, branded types, and tree shaking are the reasons it exists. If your bundle size, types, or RPC subscription story matters, the migration is worth it.

Keep reading

Get new articles in your inbox

Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.

@solana/kit: the modern JS client, functional + tree-shakeable | devrels.xyz