npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@teshies/shape

v0.0.13

Published

A simple type-safe GraphQL schema builder

Readme

@teshies/shape

A type-safe GraphQL schema builder for TypeScript with first-class Grafast integration.

npm install @teshies/shape

Peer dependencies: graphql, grafast

Quick start

import n, { makeSchema } from "@teshies/shape";

const User = n.objectType({
  name: "User",
  definition(t) {
    t.field("id", {
      type: n.nonNull(n.id),
      plan: ($source) => $source.get("id"),
    });
    t.field("name", {
      type: n.string,
      plan: ($source) => $source.get("name"),
    });
  },
});

const Query = n.objectType({
  name: "Query",
  definition(t) {
    t.field("user", {
      type: User,
      args: {
        id: { type: n.nonNull(n.id) },
      },
      plan: (_, { $id }) => loadOne($id, batchGetUserById),
    });
  },
});

const schema = makeSchema(n, {
  types: [User, Query],
});

makeSchema takes a builder instance and a config with your types. It returns a standard GraphQLSchema from graphql-js.

types accepts a flat array of shapes/extensions, a dictionary record, or a namespace import (import * as types):

import * as types from "./schema";

const schema = makeSchema(n, {
  types, // Accepts a record dictionary directly
});

Builder

The default export n is a builder with methods for creating all GraphQL type shapes and extensions:

| Method | Creates | |---|---| | n.objectType(config) | Object type | | n.interfaceType(config) | Interface type | | n.inputObjectType(config) | Input object type | | n.unionType(config) | Union type | | n.enumType(config) | Enum type | | n.scalarType(config) | Custom scalar type | | n.nonNull(type) | Non-null wrapper | | n.list(type) | List wrapper | | n.extendType(type, definition) | Type extension | | n.extendQuery(definition) | Query type extension | | n.extendMutation(definition) | Mutation type extension |

Built-in scalars are available as n.string, n.int, n.float, n.boolean, n.id. When custom scalars are passed to makeSchema, they are also dynamically registered on the builder instance as camelCase properties (e.g., n.dateTime).

Independent builder instances

While the default export n is a shared builder, you can create isolated builder instances with createBuilder(). This is useful for multi-tenant, microservice, or test environments:

import { createBuilder, makeSchema } from "@teshies/shape";

const builder = createBuilder();
const schema = makeSchema(builder, {
  types: [/* ... */],
});

Defining types

Object types and interfaces

Object types and interfaces share the same definition API. The definition callback receives a builder t with t.field(), t.connection(), and t.implements():

const Node = n.interfaceType({
  name: "Node",
  definition(t) {
    t.field("id", { type: n.nonNull(n.id) });
  },
});

const User = n.objectType({
  name: "User",
  planType: ($specifier) => userResource.get($specifier),
  definition(t) {
    t.implements(Node);
    t.field("id", {
      type: n.nonNull(n.id),
      plan: ($source) => $source.get("id"),
    });
    t.field("name", {
      type: n.string,
      plan: ($source) => $source.get("name"),
    });
  },
});

Input objects

Define input types for mutations or field arguments. You can also pass isOneOf: true to create a @oneOf input object type:

const CreateUserInput = n.inputObjectType({
  name: "CreateUserInput",
  definition(t) {
    t.field("name", { type: n.nonNull(n.string) });
    t.field("email", { type: n.string });
  },
});

// Input object with GraphQL @oneOf directive
const SearchByInput = n.inputObjectType({
  name: "SearchByInput",
  isOneOf: true,
  definition(t) {
    t.field("userId", { type: n.id });
    t.field("email", { type: n.string });
  },
});

Enums

Members can be an array of strings or a record with values:

const Role = n.enumType({
  name: "Role",
  members: ["ADMIN", "USER", "GUEST"],
});

// Or with explicit values:
const Status = n.enumType({
  name: "Status",
  members: {
    ACTIVE: { value: "active" },
    INACTIVE: { value: "inactive" },
  },
});

Unions

const SearchResult = n.unionType({
  name: "SearchResult",
  definition(t) {
    t.members(User, Post);
  },
});

Scalars

Wrap an existing GraphQLScalarType with source, or define one from scratch:

import { GraphQLDateTimeISO } from "graphql-scalars";

// Wrap an existing scalar — uses it directly
const DateTime = n.scalarType({
  name: "DateTimeISO",
  source: GraphQLDateTimeISO,
});

// Different name — clones the scalar with the new name
const Date = n.scalarType({
  name: "Date",
  source: GraphQLDateTimeISO,
});

// Custom scalar from scratch
const Json = n.scalarType({
  name: "Json",
  serialize: (value) => JSON.stringify(value),
  parseValue: (value) => JSON.parse(value as string),
});

Schema composition

Extending types

Add fields to existing types without modifying their original definition. This is useful for splitting a schema across multiple files:

// Extend the Query type with a connection field
const QueryUsers = n.extendQuery((t) => [
  t.connection("allUsers", {
    nodeType: User,
    nonNull: true,
    plan: () => loadMany(constant(null), fetchAllUsers),
  }),
]);

// Extend the Mutation type
const MutationUsers = n.extendMutation((t) => [
  t.field("createUser", {
    type: n.nonNull(User),
    args: {
      name: { type: n.nonNull(n.string) },
    },
  }),
]);

// Extend any named type
const UserExtras = n.extendType(User, (t) => [
  t.field("posts", { type: n.list(Post) }),
]);

// Pass extensions alongside types in makeSchema
makeSchema(n, {
  types: [User, Post, Query, QueryUsers, MutationUsers, UserExtras],
});

extendQuery and extendMutation automatically register Query or Mutation root types if they are not explicitly provided in types. The definition callback returns an array of field markers for type inference. Extensions are applied during schema construction.

Relay connections

t.connection() generates *Connection and *Edge types automatically:

const Query = n.objectType({
  name: "Query",
  definition(t) {
    t.connection("allUsers", {
      nodeType: User,
      enableTotalCount: true,
      enablePageInfo: true,
      nonNull: true,
    });
  },
});

This generates QueryAllUsersConnection (with nodes, edges, totalCount, pageInfo) and QueryAllUsersEdge (with node, cursor).

Options:

| Option | Default | Description | |---|---|---| | nodeType | required | The type of nodes in the connection | | args | — | Additional custom arguments for the connection field | | enableTotalCount | false | Add a totalCount field | | enablePageInfo | false | Add a non-null pageInfo: PageInfo! field (requires a PageInfo type in your schema) | | nonNull | false | Make all connection, list (nodes, edges), and item (node) fields non-null | | nonNullConn | false | Make the connection field itself non-null (users: QueryUsersConnection!) | | nonNullList | false | Make nodes and edges lists non-null ([User]!, [QueryUsersEdge]!) | | nonNullItem | false | Make individual items in lists non-null ([User!], node: User!) | | extendConnection | — | Add custom fields to the connection type | | extendEdge | — | Add custom fields to the edge type | | grafast | — | Grafast step/plan configuration for connection and edge types ({ connection, edge }) |

Note: Edge cursor: String! is always non-null regardless of nonNull settings.

Customizing Connection and Edge fields

Use extendConnection and extendEdge to add custom fields to generated Connection and Edge types:

const Query = n.objectType({
  name: "Query",
  definition(t) {
    t.connection("allUsers", {
      nodeType: User,
      extendConnection(t) {
        t.field("totalActive", { type: n.int });
      },
      extendEdge(t) {
        t.field("score", { type: n.float });
      },
    });
  },
});

Grafast & Codegen

Grafast integration

Object and interface fields accept plan and subscribePlan functions inline, as shown in the examples above. For other Grafast extensions (type-level planType, assertStep, input field applyPlan, connection plans, etc.), see the Grafast documentation.

Schema Codegen

makeSchema can generate TypeScript types for your schema. Pass a codegen option:

const schema = makeSchema(n, {
  types,
  codegen: {
    output: "./src/.generated.ts",
    schemaTypesPath: "./schema",
  },
});

Codegen runs only when NODE_ENV=development. It generates resolver types, Grafast plan types, and DeclaredTypesStrict / DeclaredTypes utility types.

Note on schemaTypesPath: schemaTypesPath (e.g. "./schema") is resolved relative to the output file directory (output: "./src/.generated.ts" -> ./src/schema.ts). Type alias exports captured from schemaTypesPath must be named starting with $ followed by an uppercase letter (e.g. $User, $QueryUsers).

Type-safe plan resolvers with DeclaredTypesStrict

The generated file exports DeclaredTypesStrict, a utility type that maps each shape to the types it owns. You use it to declare the Grafast step types for your shapes:

import type { DeclaredTypesStrict } from "./.generated";
import type { LoadedRecordStep, Maybe, Step } from "grafast";

type UserItem = { id: string; name?: string };

export type $User = DeclaredTypesStrict<{
  $: typeof User;
  User: {
    source: LoadedRecordStep<UserItem>;
    nullable: Step<Maybe<UserItem>>;
  };
}>;

$ is the shape instance that owns the declaration. The remaining keys are the GraphQL type names that belong to that shape. Each value defines the Grafast step types (source for the non-null step, nullable for the nullable step).

Once declared, plan functions on that shape's fields become fully typed:

const User = n.objectType({
  name: "User",
  definition(t) {
    t.field("name", {
      type: n.string,
      // $source is typed as LoadedRecordStep<UserItem>
      plan: ($source) => $source.get("name"),
    });
  },
});

For extensions, $ takes the extension instance and its key (a union of TypeName.fieldName) maps to the owned types:

export type $QueryUsers = DeclaredTypesStrict<{
  $: typeof QueryUsers;
  QueryAllUsersConnection: {};
  QueryAllUsersEdge: {};
}>;

DeclaredTypesStrict validates at compile time that you declare exactly the types the shape owns, catching mismatches early.

Standalone GraphQL Codegen integration

If you run @graphql-codegen/cli independently outside of makeSchema, @teshies/shape exposes its internal plugins via package entry points:

// codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  schema: "./schema.graphql",
  generates: {
    "./src/.generated.ts": {
      plugins: [
        "typescript",
        "@teshies/shape/codegen-grafast",
        {
          "@teshies/shape/codegen-shape": {
            schemaTypesPath: "./schema",
          },
        },
      ],
    },
  },
};

export default config;

How it works

Schema construction happens in two phases:

  1. Registration & Extension — All types are registered in an internal type map. Each type's definition() runs, and extensions are applied. Type references can be strings (forward references) or shape instances.

  2. Resolution & GraphQL Instantiation — Once all types are registered, toGraphQL() is called on each shape. String references resolve to real instances, interface fields are inherited, and graphql-js types are instantiated.

Each makeSchema() call creates its own isolated type registry, so the same types can be passed to multiple schema builds.

Acknowledgments

The definition API (definition(t) { t.field(...) }, extendType, extendQuery) is inspired by Nexus. Shape drops Nexus's builder shortcuts like t.string("name") and t.nonNull.string(...) in favor of explicit type composition (t.field("name", { type: n.nonNull(n.string) })). Types are values you compose rather than methods on the builder, which keeps the API closer to how graphql-js works and avoids a Nexus-specific vocabulary.

License

MIT