Skip to content

enssdk (TypeScript)

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 directly 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.15.2
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"],
"types": ["node"],
"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/hosted-instances
const ENSNODE_URL = process.env.ENSNODE_URL ?? "https://api.v2-sepolia.ensnode.io";
// 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 } 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
canonical { name { beautified } }
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.canonical ? domain.canonical.name.beautified : "<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.
  • canonical is null for non-canonical Domains (e.g. Domains whose name cannot be inferred). When non-null, canonical.name.beautified is the Domain’s Canonical Name, beautified for display. 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
canonical { name { beautified } }
owner { address }
subdomains(first: 20) {
totalCount
edges {
node { canonical { name { beautified } } 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.canonical ? domain.canonical.name.beautified : "<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.canonical ? node.canonical.name.beautified : "<unnamed>";
console.log(` - ${subName} — Owner ${node.owner?.address ?? "0x0"}`);
}
}

Notice we’re now writing the same canonical/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 (canonical { name { beautified } }, 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 } 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/hosted-instances
const ENSNODE_URL = process.env.ENSNODE_URL ?? "https://api.v2-sepolia.ensnode.io";
const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph);
const DomainFragment = graphql(`
fragment DomainFragment on Domain {
__typename
canonical { name { beautified } }
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.canonical ? domain.canonical.name.beautified : "<unnamed>";
const owner = domain.owner?.address ?? "0x0 (means reserved for ENSv2)";
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.

Point at a hosted ENSNode and go:

Terminal window
ENSNODE_URL=https://api.v2-sepolia.ensnode.io npm start

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

  • See Omnigraph examples for ready-to-copy GraphQL 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.