Basic Usage
Walk through your first end-to-end typograph app — schema, server, and typed client.
This page walks through the smallest possible typograph app. By the end of it you'll have a typed schema, a working GraphQL server, and a client that knows the shape of every response without you ever writing a type by hand.
If you'd rather skip the walkthrough and read the reference, jump straight to Schemas.
1. Define your schema
The schema is a normal TypeScript file. You build it with the
createTypeDefBuilder helper and the t shorthand for scalars, then
combine everything into a single typeDefs object that both the
server and the client will import.
import { createTypeDefBuilder, t } from "@overstacked/typograph";
const builder = createTypeDefBuilder();
const post = builder.type({
id: t.string(),
title: t.string(),
body: t.string(),
});
export const typeDefs = builder.combineTypeDefs([
builder.typeDef({
Post: post,
Query: {
listPosts: builder.query({
input: t.type({}),
output: t.type<Post[]>("[Post]"),
}),
},
Mutation: {
createPost: builder.mutation({
input: t.type({
title: t.string().notNull(),
body: t.string().notNull(),
}),
output: t.type<Post>("Post"),
}),
},
}),
]);
export type Post = typeof post;
export type TypeDefs = typeof typeDefs;A few things worth noticing:
Postis exported astypeof post— that's the bridge from the runtime builder to the static TypeScript type. You'll use it anywhere you need aPostreference.t.type<Post[]>("[Post]")is how you tell typograph "this returns a list of posts". The TypeScript generic carries the static type, and the string carries the GraphQL syntax that gets emitted in the SDL.TypeDefsis the type the client and the server both import. It's the single source of truth for everything that follows.
2. Write a resolver
The Resolvers<TypeDefs> type derives a fully-typed resolver shape
from the schema you just defined. The keys are your type names, and
the values are typed handlers for each field.
import type { Resolvers } from "@overstacked/typograph";
import type { TypeDefs } from "./schema";
const posts: Array<{ id: string; title: string; body: string }> = [];
export const resolvers: Resolvers<TypeDefs> = {
Query: {
listPosts: (_source, _args) => posts,
},
Mutation: {
createPost: (_source, { title, body }) => {
const post = { id: String(posts.length + 1), title, body };
posts.push(post);
return post;
},
},
};If you change the schema — rename a field, drop one, add a required input — the resolver file lights up red in your editor immediately.
3. Run a GraphQL server
Typograph emits standard SDL via typeDefs.toSDL(), and its
resolvers follow graphql-js's (source, args, context, info) calling
convention — so the schema and resolvers plug straight into any
graphql-js–based server with no adapter. Here's the same schema
served by graphql-yoga.
import { createServer } from "http";
import { createYoga, createSchema } from "graphql-yoga";
import { typeDefs } from "./schema";
import { resolvers } from "./resolvers";
createServer(
createYoga({
schema: createSchema({
typeDefs: typeDefs.toSDL(),
resolvers,
}),
}),
).listen(3001);See the graphql-yoga integration page for the full server setup, including subscriptions over SSE.
4. Query it from a client
This is where the magic happens. The client only needs the same
typeDefs object — no schema introspection, no codegen, no
duplicated type definitions.
import { createClient } from "@overstacked/typograph";
import { typeDefs } from "./schema";
const client = createClient(typeDefs);
const res = client.query({
listPosts: {
id: true,
title: true,
},
});
// Send the query with whatever transport you like:
const response = await fetch("http://localhost:3001/graphql", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
query: res.toGraphQL(),
variables: res.variables,
}),
});
const { data } = (await response.json()) as {
data: typeof res.returnType;
};
data.listPosts[0].title;
// ^? stringA few things to notice:
- You wrote a JavaScript object, and
res.toGraphQL()turned it into a real GraphQL string. datais fully typed from the schema — every field you selected shows up, every field you didn't doesn't, and TypeScript catches typos at compile time.- The actual transport is up to you. We used
fetchhere, but you could just as easily passres.toGraphQL()andres.variablesto urql, Apollo, graphql-request, React Query, or anything else that speaks GraphQL.
5. Or use it from React
If you're building a React app, typograph ships first-party
integrations for urql, Apollo Client, and
React Query — each gives you typed useQuery
/ useMutation (and useSubscription, where the underlying client
supports it) with no extra wiring. Pick whichever you already use.
The urql setup looks like this:
import { Client, cacheExchange, fetchExchange } from "urql";
import { createUrqlIntegration } from "@overstacked/typograph/integrations/urql";
import { typeDefs } from "./schema";
export const client = new Client({
url: "http://localhost:3001/graphql",
exchanges: [cacheExchange, fetchExchange],
});
export const { useQuery, useMutation } =
createUrqlIntegration(typeDefs);import { useQuery, useMutation } from "./urql-client";
export const Posts = () => {
const [{ data, fetching }] = useQuery({
listPosts: { id: true, title: true },
});
const [, createPost] = useMutation({
createPost: { id: true, title: true },
});
if (fetching) return <p>Loading…</p>;
return (
<>
<ul>
{data?.listPosts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
<button onClick={() => createPost({ title: "Hello", body: "World" })}>
New post
</button>
</>
);
};That's the whole loop. You wrote the schema once, and every other file pulls types directly from it.
Where to next
Schemas
Learn how to define types, queries, mutations, subscriptions, and field arguments.
Selection sets
The full client API — variables, args, dynamic selections, and __typename.
React integrations
Drop-in typed hooks for urql, Apollo Client, or React Query.
Use it with anything
Pair typograph with graphql-request, fetch, or your own client.