All articles
solanatypescripttoolinganchorcodama

codama: how the modern Solana stack generates TypeScript clients

codama auto-generates TypeScript clients from your Solana program's IDL. Why the modern Solana stack uses it instead of writing client code by hand.

For most of Solana's history, "how do I call my program from TypeScript" had one answer: use Anchor's client. You shipped your IDL alongside your program, the client loaded it at runtime, and you got back a chainable program.methods.foo(...) builder.

That works. It's also slow (runtime IDL parsing), bundle-heavy (the whole Anchor client lands in your app even if you only call one instruction), and weakly typed (your types are inferred fromany-shaped IDL JSON, not a real schema).

codama is the modern alternative: a code generator that takes an IDL (Anchor IDL, Shank IDL, or its own format) and emits standalone TypeScript files with one tree-shakeable function per instruction, fully typed, zero runtime IDL.

The mental model

Anchor client = runtime interpreter. Your bundle ships the IDL and an interpreter that walks it on every call.

codama = build-time compiler. Your bundle ships the generated instruction functions and nothing else. The IDL is consumed at build time and discarded.

It's the same shift TypeScript made when tsc replaced runtime type-checking. You give up the ability to introspect program shape at runtime; you get bundle size, type safety, and tree-shaking in return.

The generated output

Given an Anchor IDL for a program with initialize, deposit, and withdraw instructions, codama emits something like:

typescript
// Generated by codama — don't edit
import { Address, IInstruction } from "@solana/web3.js"

export type DepositInstruction = IInstruction<typeof MY_PROGRAM_ADDRESS, [
  { address: Address; role: WritableSignerAccountRole },   // payer
  { address: Address; role: WritableAccountRole },          // vault
  { address: Address; role: ReadonlyAccountRole },          // mint
  { address: Address; role: ReadonlyAccountRole },          // tokenProgram
]>

export type DepositInstructionData = {
  discriminator: ReadonlyUint8Array
  amount: bigint
}

export function getDepositInstruction(input: {
  payer: TransactionSigner
  vault: Address
  mint: Address
  tokenProgram?: Address
  amount: bigint | number
}): DepositInstruction { /* ... */ }

Strongly typed inputs, strongly typed outputs, no runtime IDL parsing, and your bundler can tree-shake the instructions you don't use.

The flow

Three commands, run as part of your build pipeline:

sh
# 1. Install
npm install --save-dev codama @codama/nodes-from-anchor @codama/renderers-js

# 2. Generate (a one-liner script)
node -e "
import { rootNodeFromAnchor } from '@codama/nodes-from-anchor'
import { createFromRoot } from 'codama'
import { renderVisitor } from '@codama/renderers-js'

const idl = require('./target/idl/my_program.json')
const codama = createFromRoot(rootNodeFromAnchor(idl))
codama.accept(renderVisitor('./src/generated'))
"

# 3. Use the generated functions
import { getDepositInstruction } from './generated'

Most projects wire that script into package.json as a build step:

json
{
  "scripts": {
    "generate:client": "node ./scripts/codama.mjs",
    "build": "npm run generate:client && tsc"
  }
}

Now your client is regenerated every time you build, always in sync with the IDL, and the diff shows up in git so reviewers can see protocol-shape changes before merging.

Using the output with @solana/kit

codama's default renderer targets @solana/kit (the modern web3.js successor). The generated instruction functions plug directly into kit's transaction pipeline:

typescript
import { pipe, createTransactionMessage, setTransactionMessageFeePayer, appendTransactionMessageInstruction, signAndSendTransactionMessageWithSigners } from "@solana/kit"
import { getDepositInstruction } from "./generated"

const ix = getDepositInstruction({
  payer,
  vault: vaultAddress,
  mint: usdcMint,
  amount: 1_000_000n,
})

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

const signature = await signAndSendTransactionMessageWithSigners(tx)

What codama doesn't do

  • It doesn't write your program. You still author the Rust (Anchor, Pinocchio, Steel, raw). codama only consumes the IDL artifact.
  • It doesn't handle account fetching. For getProgramAccounts and account deserialization you still call kit directly — though codama generates the deserializers for you.
  • It doesn't replace Anchor on the program side. Programs can still be Anchor; you just stop using Anchor's TS client.

When to switch

Switch to codama if: you ship a frontend that's bundle-size sensitive, you want first-class TypeScript types for your program, you're migrating to @solana/kit anyway, or you're writing a server-side client where types catch bugs you wouldn't notice until runtime.

Stay on Anchor's client if: your codebase is deeply Anchor-shaped already, your frontend bundle size genuinely doesn't matter, or you're doing rapid prototyping where the IDL changes hourly and you don't want a build step.

References

codama isn't a productivity hack — it's a quiet architectural shift in how Solana frontends get built. If you're starting a new TypeScript project against a program in 2026, this is the default.

codama: how the modern Solana stack generates TypeScript clients | devrels.xyz