graphql-codegen automates the generation of TypeScript types and helper functions from GraphQL schemas and operations, ensuring your frontend code stays in sync with your API. graphql-tag is a lightweight utility for parsing GraphQL query strings into ASTs, enabling template literal tagging in JavaScript. graphql-tools provides a suite of utilities for mocking, stitching, and managing GraphQL schemas on the server side, though its core features have largely migrated to the graphql package itself. optics-ts offers a functional approach to immutable data updates using lenses, which can be useful for managing complex nested state in GraphQL cache normalization or form handling, but it is not specific to GraphQL.
When architecting a GraphQL system in the TypeScript ecosystem, developers often encounter four distinct packages that solve very different problems. graphql-codegen, graphql-tag, graphql-tools, and optics-ts are frequently listed together, but they operate at different layers of the stack. Understanding when to use each—and when to avoid them—is critical for maintaining a clean, type-safe, and performant codebase.
These packages do not compete; they complement each other or serve entirely different niches.
graphql-codegen is a build-time tool. It reads your schema and queries to generate TypeScript code. It lives in your development workflow.graphql-tag is a runtime parser. It turns template strings into GraphQL ASTs so libraries like Apollo Client can read them. It lives in your bundled code.graphql-tools is a server-side utility belt. It helps create, mock, and stitch schemas. It lives on your backend.optics-ts is a general functional utility. It helps update immutable data. It lives in your state management logic, unrelated to GraphQL specifically.graphql-codegenThe biggest challenge in GraphQL frontends is keeping TypeScript types in sync with your backend schema. Manual typing is error-prone and slow. graphql-codegen solves this by generating types automatically whenever your schema changes.
graphql-codegen generates full TypeScript interfaces and React hooks based on your actual queries.
// Generated by graphql-codegen
export type GetUserQuery = {
__typename?: 'Query';
user?: {
__typename?: 'User';
id: string;
email: string;
} | null;
};
// It also generates ready-to-use hooks
import { useGetUserQuery } from '@/generated/graphql';
function Profile() {
const { data, loading } = useGetUserQuery({ variables: { id: '123' } });
// 'data' is fully typed. No 'any' needed.
return <div>{data?.user?.email}</div>;
}
Without this tool, you would have to manually define these interfaces, risking mismatches if the backend adds a field or changes a type.
graphql-tag vs. Modern AlternativesHistorically, GraphQL clients needed a way to parse query strings inside JavaScript template literals. graphql-tag was the standard solution for this, wrapping strings in a tagged template function.
graphql-tag parses the string at runtime (or build time) into an AST object.
import gql from 'graphql-tag';
const query = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`;
// 'query' is now an AST object, not a string
However, in modern workflows, graphql-codegen often makes graphql-tag redundant. When you configure codegen to import from .graphql files, your bundler handles the parsing, and you get better type safety without the runtime overhead of the gql tag.
// Modern approach with graphql-codegen and .graphql files
import { useGetUserQuery } from '@/generated/graphql';
import GetUserDocument from './GetUser.graphql'; // Loaded as a typed document
// No need for 'gql' tag; types are inferred directly from the file
If you are starting a new project today, prefer the file-based approach with graphql-codegen over inline gql tags unless you have a specific dynamic requirement.
graphql-toolsOn the server side, graphql-tools was once the go-to library for making executable schemas, mocking data, and stitching multiple schemas together. However, the landscape has shifted.
graphql-tools previously provided makeExecutableSchema to combine type definitions with resolvers.
// Legacy pattern using graphql-tools
import { makeExecutableSchema } from '@graphql-tools/schema';
const schema = makeExecutableSchema({
typeDefs: `
type Query {
hello: String
}
`,
resolvers: {
Query: {
hello: () => 'Hello world'
}
}
});
Today, most of these utilities have moved into the core graphql package or are built directly into server frameworks like Apollo Server v4+ and GraphQL Yoga. The makeExecutableSchema function, for instance, is now available via @graphql-tools/schema, but many developers find they don't need it at all when using modern server constructors that accept typeDefs and resolvers directly.
// Modern pattern with GraphQL Yoga or Apollo Server v4
import { createYoga } from 'graphql-yoga';
const yoga = createYoga({
schema: {
typeDefs: `type Query { hello: String }`,
resolvers: { Query: { hello: () => 'Hello' } }
}
});
// No need for explicit 'makeExecutableSchema' in many cases
Use graphql-tools only if you need advanced features like schema stitching (combining multiple microservice schemas) or complex mocking that isn't supported by your server framework's defaults.
optics-ts Fits (and Doesn't)optics-ts is the outlier here. It is not a GraphQL library. It is a functional programming tool for updating immutable data using "lenses." While GraphQL clients (like Apollo or Relay) manage immutable cache data, optics-ts is a general-purpose utility for any immutable structure.
optics-ts allows you to update deeply nested properties without writing nested spread operators.
import { optic, set } from 'optics-ts';
interface State {
user: {
profile: {
settings: {
theme: string;
};
};
};
}
const oldState: State = { user: { profile: { settings: { theme: 'light' } } } };
// Using standard TypeScript (verbose)
const newState1 = {
...oldState,
user: {
...oldState.user,
profile: {
...oldState.user.profile,
settings: {
...oldState.user.profile.settings,
theme: 'dark'
}
}
}
};
// Using optics-ts (concise and type-safe)
const themeOptic = optic<State>().prop('user').prop('profile').prop('settings').prop('theme');
const newState2 = set(themeOptic)('dark')(oldState);
You might use this in a GraphQL app if you are writing custom cache updates or managing complex local state outside of your GraphQL client's automatic normalization. However, do not install it expecting it to help with GraphQL queries or schemas; it solves a state management problem, not a GraphQL problem.
| Feature | graphql-codegen | graphql-tag | graphql-tools | optics-ts |
|---|---|---|---|---|
| Primary Role | Build-time Type Generation | Runtime Query Parsing | Server Schema Utilities | Immutable Data Updates |
| Language Support | TypeScript (Generates TS) | JavaScript/TS | JavaScript/TS | TypeScript |
| Execution Time | Development / Build | Runtime (or Build) | Runtime (Server) | Runtime |
| GraphQL Specific | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No (General Utility) |
| Modern Alternative | N/A (Industry Standard) | .graphql files + Codegen | Native graphql / Server Frameworks | Immer, Structurizr |
Start with graphql-codegen: If you are using TypeScript with GraphQL, this is non-negotiable. It provides the highest return on investment by automating types and hooks. Configure it to generate code from .graphql files to avoid relying on graphql-tag.
Avoid graphql-tag in New Projects: Unless you are dynamically constructing queries at runtime (a rare pattern), use static .graphql files processed by graphql-codegen. This reduces bundle size and improves type inference.
Audit graphql-tools Usage: If you are setting up a new server, check if your framework (Apollo, Yoga, Envelop) handles schema creation natively. Only add graphql-tools if you need specific advanced features like schema stitching or detailed mocking strategies that your framework lacks.
Use optics-ts Only for Complex State: Do not add this to your project just because you are using GraphQL. Only introduce it if you find yourself struggling with deeply nested immutable updates in your local state management and standard spread syntax becomes unreadable.
For a modern, professional GraphQL stack:
graphql-codegen for types and hooks. Use static .graphql files instead of graphql-tag.graphql-tools only for advanced stitching or mocking.optics-ts unless you have a specific, complex immutable data problem that standard patterns cannot solve cleanly.By separating these concerns, you build a system that is type-safe, performant, and easy to maintain, without carrying unnecessary dependencies.
Choose graphql-tag only if you are working in a legacy environment or a specific build setup that requires parsing GraphQL strings at runtime without a build-step transformer. For most modern projects using Webpack, Vite, or Next.js, this package is unnecessary because bundlers handle .graphql files natively or via graphql-codegen.
Choose graphql-tools if you are maintaining an older GraphQL server that relies on schema stitching or specific mocking utilities not yet migrated to the core graphql package. For new server projects, prefer the native capabilities of graphql-js or modern server frameworks like Apollo Server or Yoga, as many graphql-tools features are now deprecated or merged upstream.
Choose optics-ts if you need a robust, type-safe way to update deeply nested immutable data structures in your state management layer (like Redux or Zustand) and find standard spread operators too verbose. Do not choose this for GraphQL-specific tasks; it is a general-purpose functional utility library and does not interact with GraphQL schemas or queries directly.
Choose graphql-codegen if you are building a TypeScript application (React, Vue, Angular, or Node) that consumes a GraphQL API. It is essential for projects requiring strict type safety between your frontend queries and the backend schema, eliminating manual type definition and reducing runtime errors. It is the industry standard for generating hooks (like useQuery) and types automatically.
Helpful utilities for parsing GraphQL queries. Includes:
gql A JavaScript template literal tag that parses GraphQL query strings into the standard GraphQL AST./loader A webpack loader to preprocess queriesgraphql-tag uses the reference graphql library under the hood as a peer dependency, so in addition to installing this module, you'll also have to install graphql.
The gql template literal tag can be used to concisely write a GraphQL query that is parsed into a standard GraphQL AST. It is the recommended method for passing queries to Apollo Client. While it is primarily built for Apollo Client, it generates a generic GraphQL AST which can be used by any GraphQL client.
import gql from 'graphql-tag';
const query = gql`
{
user(id: 5) {
firstName
lastName
}
}
`
The above query now contains the following syntax tree.
{
"kind": "Document",
"definitions": [
{
"kind": "OperationDefinition",
"operation": "query",
"name": null,
"variableDefinitions": null,
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"alias": null,
"name": {
"kind": "Name",
"value": "user",
...
}
}
]
}
}
]
}
The gql tag can also be used to define reusable fragments, which can easily be added to queries or other fragments.
import gql from 'graphql-tag';
const userFragment = gql`
fragment User_user on User {
firstName
lastName
}
`
The above userFragment document can be embedded in another document using a template literal placeholder.
const query = gql`
{
user(id: 5) {
...User_user
}
}
${userFragment}
`
Note: While it may seem redundant to have to both embed the userFragment variable in the template literal AND spread the ...User_user fragment in the graphQL selection set, this requirement makes static analysis by tools such as eslint-plugin-graphql possible.
GraphQL strings are the right way to write queries in your code, because they can be statically analyzed using tools like eslint-plugin-graphql. However, strings are inconvenient to manipulate, if you are trying to do things like add extra fields, merge multiple queries together, or other interesting stuff.
That's where this package comes in - it lets you write your queries with ES2015 template literals and compile them into an AST with the gql tag.
This package only has one feature - it caches previous parse results in a simple dictionary. This means that if you call the tag on the same query multiple times, it doesn't waste time parsing it again. It also means you can use === to compare queries to check if they are identical.
To add support for importing .graphql/.gql files, see Webpack loading and preprocessing below.
Given a file MyQuery.graphql
query MyQuery {
...
}
If you have configured the webpack graphql-tag/loader, you can import modules containing graphQL queries. The imported value will be the pre-built AST.
import MyQuery from 'query.graphql'
You can also import query and fragment documents by name.
query MyQuery1 {
...
}
query MyQuery2 {
...
}
And in your JavaScript:
import { MyQuery1, MyQuery2 } from 'query.graphql'
Preprocessing GraphQL queries and fragments into ASTs at build time can greatly improve load times.
GraphQL queries can be compiled at build time using babel-plugin-graphql-tag. Pre-compiling queries decreases script initialization time and reduces bundle sizes by potentially removing the need for graphql-tag at runtime.
Try this custom transformer to pre-compile your GraphQL queries in TypeScript: ts-transform-graphql-tag.
Preprocessing queries via the webpack loader is not always possible. babel-plugin-import-graphql supports importing graphql files directly into your JavaScript by preprocessing GraphQL queries into ASTs at compile-time.
E.g.:
import myImportedQuery from './productsQuery.graphql'
class ProductsPage extends React.Component {
...
}
Using the included graphql-tag/loader it is possible to maintain query logic that is separate from the rest of your application logic. With the loader configured, imported graphQL files will be converted to AST during the webpack build process.
Example webpack configuration
{
...
loaders: [
{
test: /\.(graphql|gql)$/,
exclude: /node_modules/,
loader: 'graphql-tag/loader'
}
],
...
}
Preprocessing GraphQL imports is supported in create-react-app >= v2 using evenchange4/graphql.macro.
For create-react-app < v2, you'll either need to eject or use react-app-rewire-inline-import-graphql-ast.
Testing environments that don't support Webpack require additional configuration. For Jest use jest-transform-graphql.
With the webpack loader, you can import fragments by name:
In a file called query.gql:
fragment MyFragment1 on MyType1 {
...
}
fragment MyFragment2 on MyType2 {
...
}
And in your JavaScript:
import { MyFragment1, MyFragment2 } from 'query.gql'
Note: If your fragment references other fragments, the resulting document will
have multiple fragments in it. In this case you must still specify the fragment name when using the fragment. For example, with @apollo/client you would specify the fragmentName option when using the fragment for cache operations.
This package will emit a warning if you have multiple fragments of the same name. You can disable this with:
import { disableFragmentWarnings } from 'graphql-tag';
disableFragmentWarnings()
This package exports an experimentalFragmentVariables flag that allows you to use experimental support for parameterized fragments.
You can enable / disable this with:
import { enableExperimentalFragmentVariables, disableExperimentalFragmentVariables } from 'graphql-tag';
Enabling this feature allows you to declare documents of the form.
fragment SomeFragment ($arg: String!) on SomeType {
someField
}
You can easily generate and explore a GraphQL AST on astexplorer.net.