Skip to content

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.

If you already have a TypeScript project, skip ahead to Install enssdk.

Otherwise:

Terminal window
mkdir my-ens-script && cd my-ens-script
npm init -y
mkdir src

We’ll use tsx to run TypeScript directly without a bundler.

Terminal window
npm install enssdk@1.13.1
npm install -D tsx typescript @types/node

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:

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:

.vscode/settings.json
{
"js/ts.tsdk.path": "node_modules/typescript/lib",
"js/ts.tsdk.promptToUseWorkspaceVersion": true
}

Also add a start script to package.json:

package.json
{
"type": "module",
"scripts": {
"start": "tsx src/index.ts"
}
}

The EnsNodeClient is the entry point. Extend it with the omnigraph module to get the client.omnigraph.query(...) method.

src/index.ts
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-instances
const ENSNODE_URL = process.env.ENSNODE_URL!;
// create and extend an EnsNodeClient with Omnigraph support
const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph);

Add your first query — look up the eth Domain and print its owner and protocol version.

src/index.ts
// 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 over result.data and you’ll see it’s typed exactly to your selection set — try removing owner { address } from the query and watch the access below become a type error.
  • domain is a union of ENSv1Domain | ENSv2Domain (both implement the Domain interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — __typename tells you which one you got.
  • name is null for non-canonical Domains (e.g. Domains whose name cannot be inferred). Always guard the access; TypeScript will help you.

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.

src/index.ts
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.

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.

src/index.ts
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-instances
const 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 ...DomainFragmentformatDomain accepts any of them, including each node in the subdomain edges. readFragment(DomainFragment, data) unwraps that opaque type to the typed fields you declared.

Point at a hosted ENSNode and go:

Terminal window
ENSNODE_URL=https://api.alpha.green.ensnode.io npm start

You should see the eth Domain, followed by its first 20 subdomains and the total subdomain count.

  • 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 — same graphql(...) helper, with useOmnigraphQuery and a graphcache.