# 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`](https://gql-tada.0no.co/)).

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](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example).

<IntegrateHostedEnsNodeTip />

## 1. Scaffold a TypeScript project

If you already have a TypeScript project, skip ahead to [Install `enssdk`](#2-install-enssdk).

Otherwise:

```sh
mkdir my-ens-script && cd my-ens-script
npm init -y
mkdir src
```

## 2. Install enssdk

We'll use [`tsx`](https://tsx.is/) to run TypeScript directly without a bundler.

```
`npm install enssdk@${snapshot.sdkVersion
```
**Pin exact versions:** Always 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, and matched to the ENSNode
  version your hosted instance runs.

## 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`:

```json title="tsconfig.json" ins={11-17}
{
  "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`:

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

Also add a `start` script to `package.json`:

```json title="package.json" ins={3-5}
{
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts"
  }
}
```

## 4. Construct the client

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

```ts title="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);
```

## 5. Hello world

Add your first query — look up the `eth` Domain and print its owner and protocol version.
**InterpretedName:** An **InterpretedName** is a Name whose labels are each either normalized or represented as an
  [encoded labelhash](/docs/reference/terminology#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](/docs/reference/terminology#interpreted-name) in the terminology reference.

```ts title="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);
});
```
**BeautifiedName:** `beautified` is the display-ready form of the Canonical Name — its normalized labels rendered per [ENSIP-15](https://docs.ens.domains/ensip/15) (e.g. `♾.eth` → `♾️.eth`) — so you can render it directly with no normalization or emoji logic of your own. It's **display-only**: use `interpreted` (or the Domain `id`) as a lookup key or navigation target, never `beautified`. See [Beautified Name](/docs/reference/terminology#beautified-name).

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.

## 6. List subdomains

Expand the query to also fetch the Domain's subdomains. `subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm) — pass `first: 20` to cap the page, and select `totalCount` to learn how many subdomains exist in total.

```ts title="src/index.ts" ins={7-12,34-38}
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](https://relay.dev/graphql/connections.htm).

## 7. Extract a typed fragment

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.

```ts title="src/index.ts" ins={3,11-17,23,26,31,34-40,56,59}
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.

## 8. Run it

Point at a hosted ENSNode and go:

```sh
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.

## Where to go next

- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy GraphQL queries: account-owned domains, events, registrar permissions, full-text search, and more.
- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query.
- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper, with `useOmnigraphQuery` and a graphcache.

[Interactive Example ⚡](/docs/integrate/integration-options/enssdk/example)

[enssdk-example (GitHub)](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example)

[gql.tada documentation](https://gql-tada.0no.co/)