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.

@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.