enssdk
enssdk is the foundational TypeScript/JavaScript SDK for ENS development. It’s a fully modular and tree-shakeable modern TypeScript/JavaScript package that provides ENS-specific types and helpers — usable from any JS runtime, browser or server.
enssdk also dirctly integrates with ENSNode, providing an EnsNodeClient (via createEnsNodeClient) that can be extended with a fully-typed Omnigraph API client (powered by gql.tada).
This guide walks you from an empty directory to a working TypeScript script that queries the eth Domain and queries its subdomains — the same flow as our enssdk-example.
You don’t need to run your own ENSNode to follow this guide — the steps below default to a NameHash-hosted instance. Browse the available deployments below.
1. Scaffold a TypeScript project
Section titled “1. Scaffold a TypeScript project”If you already have a TypeScript project, skip ahead to Install enssdk.
Otherwise:
mkdir my-ens-script && cd my-ens-scriptnpm init -ymkdir src2. Install enssdk
Section titled “2. Install enssdk”We’ll use tsx to run TypeScript directly without a bundler.
npm install enssdk@1.13.1npm install -D tsx typescript @types/nodeAlways pin an exact version (no ^ or ~) of enssdk. The Omnigraph GraphQL schema is bundled inside enssdk and consumed by the gql.tada TypeScript plugin to type your queries — a minor or patch bump can change the schema and silently drift your generated types away from your queries. Locking the exact version keeps types and runtime in sync.
3. Configure the gql.tada TypeScript plugin
Section titled “3. Configure the gql.tada TypeScript plugin”gql.tada is what gives your graphql(...) query strings end-to-end type safety. It reads the Omnigraph schema from enssdk at typecheck time.
Create tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "lib": ["ESNext"], "plugins": [ { "name": "gql.tada/ts-plugin", "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql", "tadaOutputLocation": "./src/generated/graphql-env.d.ts" } ] }, "include": ["src"]}If you’re using VS Code, make sure your workspace is using the workspace TypeScript version so the plugin loads. Add this to .vscode/settings.json:
{ "js/ts.tsdk.path": "node_modules/typescript/lib", "js/ts.tsdk.promptToUseWorkspaceVersion": true}Also add a start script to package.json:
{ "type": "module", "scripts": { "start": "tsx src/index.ts" }}4. Construct the client
Section titled “4. Construct the client”The EnsNodeClient is the entry point. Extend it with the omnigraph module to get the client.omnigraph.query(...) method.
import { createEnsNodeClient } from "enssdk/core";import { omnigraph } from "enssdk/omnigraph";
// you may use a NameHash Hosted ENSNode instance// learn more at https://ensnode.io/docs/integrate/hosted-instancesconst ENSNODE_URL = process.env.ENSNODE_URL!;
// create and extend an EnsNodeClient with Omnigraph supportconst client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph);5. Hello world
Section titled “5. Hello world”Add your first query — look up the eth Domain and print its owner and protocol version.
An InterpretedName is a Name whose labels are each either normalized or represented as an encoded labelhash (e.g. [abcd...].eth) — the canonical, lossless form ENSNode uses to identify a Name. asInterpretedName("eth") brands a known-safe string as one; for user input, validate first. See Interpreted Name in the terminology reference.
// existing imports...
import { asInterpretedName, beautifyInterpretedName } from "enssdk";import { graphql, omnigraph } from "enssdk/omnigraph";
// existing client...
// this is typechecked and editor autocompleted with built-in docs!const HelloWorldQuery = graphql(` query HelloWorld($name: InterpretedName!) { domain(by: { name: $name }) { __typename name owner { address } } }`);
async function main() { const name = asInterpretedName("eth");
const result = await client.omnigraph.query({ query: HelloWorldQuery, variables: { name }, });
if (result.errors) throw new Error(JSON.stringify(result.errors)); if (!result.data?.domain) throw new Error(`Domain '${name}' not found`);
const { domain } = result.data;
console.log(`Name: ${domain.name ? beautifyInterpretedName(domain.name) : "<unnamed>"}`); console.log(`Version: ${domain.__typename}`); console.log(`Owner: ${domain.owner?.address ?? "0x0"}`);}
main().catch((err) => { console.error(err); process.exit(1);});A few things to notice:
graphql(...)parses your query at typecheck time. Hover overresult.dataand you’ll see it’s typed exactly to your selection set — try removingowner { address }from the query and watch the access below become a type error.domainis a union ofENSv1Domain | ENSv2Domain(both implement theDomaininterface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query —__typenametells you which one you got.nameisnullfor non-canonical Domains (e.g. Domains whose name cannot be inferred). Always guard the access; TypeScript will help you.
6. List subdomains
Section titled “6. List subdomains”Expand the query to also fetch the Domain’s subdomains. subdomains is a Relay Connection — pass first: 20 to cap the page, and select totalCount to learn how many subdomains exist in total.
const HelloWorldQuery = graphql(` query HelloWorld($name: InterpretedName!) { domain(by: { name: $name }) { __typename name owner { address } subdomains(first: 20) { totalCount edges { node { name owner { address } } } } } }`);
async function main() { const name = asInterpretedName("eth");
const result = await client.omnigraph.query({ query: HelloWorldQuery, variables: { name }, });
if (result.errors) throw new Error(JSON.stringify(result.errors)); if (!result.data?.domain) throw new Error(`Domain '${name}' not found`);
const { domain } = result.data;
console.log(`Name: ${domain.name ? beautifyInterpretedName(domain.name) : "<unnamed>"}`); console.log(`Version: ${domain.__typename}`); console.log(`Owner: ${domain.owner?.address ?? "0x0"}`);
console.log(`\nSubdomains (showing 20 of ${domain.subdomains?.totalCount ?? 0}):`); for (const { node } of domain.subdomains?.edges ?? []) { const subName = node.name ? beautifyInterpretedName(node.name) : "<unnamed>"; console.log(` - ${subName} — Owner ${node.owner?.address ?? "0x0"}`); }}Notice we’re now writing the same name/owner rendering twice — once for the parent Domain and once inside the subdomain loop. We’ll fix that next.
To page beyond the first 20, the connection also exposes pageInfo { hasNextPage endCursor } and accepts an after: String cursor — see Relay’s connection spec.
7. Extract a typed fragment
Section titled “7. Extract a typed fragment”Notice we’re selecting the same fields (name, owner { address }) on the parent Domain and on each subdomain, and rendering them the same way. Extract a DomainFragment to deduplicate the selection — and get a reusable, fully-typed function that formats a Domain.
import { asInterpretedName, beautifyInterpretedName } from "enssdk";import { createEnsNodeClient } from "enssdk/core";import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph";
// you may use a NameHash Hosted ENSNode instance// learn more at https://ensnode.io/docs/integrate/hosted-instancesconst ENSNODE_URL = process.env.ENSNODE_URL!;
const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph);
const DomainFragment = graphql(` fragment DomainFragment on Domain { __typename name owner { address } }`);
const HelloWorldQuery = graphql( ` query HelloWorld($name: InterpretedName!) { domain(by: { name: $name }) { ...DomainFragment subdomains(first: 20) { totalCount edges { node { ...DomainFragment } } } } }`, [DomainFragment],);
function formatDomain(data: FragmentOf<typeof DomainFragment>): string { // type-safe access to fragment data! const domain = readFragment(DomainFragment, data); const name = domain.name ? beautifyInterpretedName(domain.name) : "<unnamed>"; const owner = domain.owner?.address ?? "0x0"; return `${name} (${domain.__typename}) — Owner ${owner}`;}
async function main() { const name = asInterpretedName("eth");
const result = await client.omnigraph.query({ query: HelloWorldQuery, variables: { name }, });
if (result.errors) throw new Error(JSON.stringify(result.errors)); if (!result.data?.domain) throw new Error(`Domain '${name}' not found`);
const { domain } = result.data; const totalCount = domain.subdomains?.totalCount ?? 0;
console.log(formatDomain(domain)); console.log(`\nSubdomains (showing 20 of ${totalCount}):`); for (const { node } of domain.subdomains?.edges ?? []) { console.log(` - ${formatDomain(node)}`); }}FragmentOf<typeof DomainFragment> is the opaque type for any selection that includes ...DomainFragment — formatDomain accepts any of them, including each node in the subdomain edges. readFragment(DomainFragment, data) unwraps that opaque type to the typed fields you declared.
8. Run it
Section titled “8. Run it”Point at a hosted ENSNode and go:
ENSNODE_URL=https://api.alpha.green.ensnode.io npm startYou should see the eth Domain, followed by its first 20 subdomains and the total subdomain count.
Where to go next
Section titled “Where to go next”- See the Omnigraph Cookbook for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more.
- See the Omnigraph Schema Reference for the full set of types, fields, and arguments you can query.
- Building a React app? Use
enskit— samegraphql(...)helper, withuseOmnigraphQueryand a graphcache.