graphql-tag: a template tag, a webpack loader, and a deliberately redundant fragment spread
A JavaScript template literal tag that parses GraphQL queries
At a glance
- What is it?
- The package that turns a GraphQL string into an AST, with one runtime dependency and a peer dependency you must install yourself. Its test script covers two of the three TypeScript and graphql combinations it defines, and its webpack example is written in syntax from webpack 1.
- Who is it for?
- Adopt graphql-tag if you write GraphQL in JavaScript or TypeScript and want queries that a linter can read, because the whole justification is static analysis and the redundant fragment spread is what makes it possible, and the parse cache gives you reference equality so you can skip re-sending an identical query. Do not adopt it expecting a client, a cache or a transport, because it produces an AST and stops there, and it ships only two things.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 105 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One runtime dependency, and a peer dependency you have to install
The scope of this package is small enough to state precisely, which is worth doing before anything else. It provides a template literal tag that parses GraphQL query strings into the standard AST, and a webpack loader for preprocessing queries. Nothing else. It is not a client, it does not send anything over a network, and it holds no state beyond a cache. The dependency profile is correspondingly narrow. There is a single runtime dependency, which is a TypeScript helper library, and the reference GraphQL library is a peer dependency, so you install it yourself alongside this package. That is the right arrangement for something that parses another library's document format: the version of the parser used is the version your application already has, not one this package pins. The payoff is compatibility, and the project's own test script demonstrates how seriously that is taken, since it installs a different pair of TypeScript and GraphQL versions before each run. The tag itself is one import and one call:
import gql from 'graphql-tag';The result is a generic AST. The README says the tag is the recommended method for passing queries to Apollo Client, and immediately adds that it is primarily built for Apollo Client while generating a generic AST usable by any GraphQL client. That second clause is the part to hold on to, because it means nothing in the output is tied to one client library, and the lock-in the package is designed to avoid is at the transport layer rather than the query layer.
The redundant fragment spread exists so a linter can see the name
There is a note in this README that is worth more attention than any feature description, because it is a design decision explained rather than a feature. To use a fragment you do two things that look like they overlap. You spread the fragment name inside the selection set, and you interpolate the fragment document into the template literal as a placeholder. The first is GraphQL syntax the server needs. The second looks redundant, because the interpolated document already contains the fragment definition. The note says plainly that it may seem redundant, and that the requirement makes static analysis by tools such as a GraphQL linter possible. The reason is now obvious once you see it stated. A linter reading a query needs to find the name User_user as a literal token inside the query text so it can look up the fragment definition and check the fields against the schema. If you interpolated the fragment and omitted the name, the linter would see a variable with no literal to resolve. So the redundancy is the price of a query that a tool can check, and the README is honest that you are paying it. That trade is the entire argument for this package over a plain string, because the plain string is statically analysable but awkward to manipulate, and the AST is the opposite. The tag gives you both only if you accept writing the name twice.
The cache is the second feature, and it hands you reference equality
The README describes caching in one sentence and then explains why you would want it. Previous parse results are kept in a simple dictionary, so calling the tag on the same query more than once does not parse it again. The second consequence is the one that changes how you write client code. Because the same source text produces the same cached object, you can use the identity operator to compare two queries and check whether they are identical. That matters because GraphQL clients cache and deduplicate on document identity, so two components that both include the same query end up sharing one network request rather than sending the query twice. Without the cache, the tag would return two structurally equal but distinct objects, and every request would go on the wire. So the cache is not a performance detail, it is what makes deduplication work, and it is why the package claims only one feature beyond parsing. The scope of that cache is also worth being precise about, because it is described as simple. It is a dictionary keyed by the query text, held for the life of the process, which means it is bounded by how many distinct query strings your application ever evaluates. For an application with a fixed set of queries that is nothing. For one that builds query strings dynamically, from user input or from a schema, every distinct string is a new entry, and the cache grows without a documented eviction policy. The README does not describe a size limit or a clear function, so that is the behaviour to assume.
Three TypeScript and graphql combinations, and the test script runs two
The test configuration in the project manifest is the most revealing file in the repository, and it contains a small inconsistency worth knowing about. There are three test targets, and each one installs a different pair of versions before running the same suite. The first installs TypeScript 3.7 alongside GraphQL 15. The second installs TypeScript 4 alongside GraphQL 16.5. The third installs TypeScript 6 alongside GraphQL 17. Each of those then builds the project and runs a single bundled test file. So the same source is compiled and tested against a TypeScript version from three years before the package and a GraphQL major two ahead of the one in the development dependencies. That is an unusually thorough compatibility approach for a package this small, and it is the concrete justification for the peer dependency arrangement. Now the inconsistency. The test script runs the first and the second targets and does not include the third. The TypeScript 6 and GraphQL 17 lane exists, is defined, and is not part of the default run. Two readings are possible and the repository does not say which is intended: either that lane is run manually or in a separate workflow, or it was prepared for a future default and not yet wired in. Either way, if you are on the newest TypeScript and the newest GraphQL, the combination the third target describes is the one the default test command does not check, and that is worth a question before you adopt the package on a bleeding-edge toolchain.
The webpack example is written in webpack 1 syntax
The loader is the second of the two things this package ships, and it is the part with a documented configuration that will not work as printed. Here is the example configuration from the README:
{
...
loaders: [
{
test: /\.(graphql|gql)$/,
exclude: /node_modules/,
loader: 'graphql-tag/loader'
}
],
...
}The problem is the key name. A loaders array at the top level of the configuration is webpack 1 syntax; current webpack versions expect a module object containing a rules array. The three things inside the entry are all still correct, the file extension test, the exclusion of installed modules, and the loader name, so the fix is a mechanical move of the same entry into the modern location. This is the most-copied snippet in the README, and it is the one most likely to be pasted into a build that then fails with a configuration error rather than anything about GraphQL. The broader point is the same one the loader exists to solve. With the loader configured, an imported GraphQL file becomes a pre-built AST during the build, which removes the runtime parser from the bundle and lets a bundler see the query at build time. The README is explicit that the imported value is the pre-built AST, which means code written for the tag and code written for the loader are not the same code, and the surrounding tooling has to know which mode it is in. That is also why there is a separate transform recommended for test runners that do not use webpack.
TypeScript definitions, a Flow definition file, and a field from a dead bundler era
The manifest describes three entry points and two type systems, and one of each is a historical artefact. The main field points at a file at the repository root, the module field points into the built library, and the types field points at a declaration file in the built library. The published files are the built library, the source directory, and the two files at the repository root, which explains why the loader and the main entry are visible in the top-level listing rather than hidden in a build output. Two older conventions are also present and worth naming. There is a jsnext:main field, which was a hint for a bundler convention that no longer resolves in modern tooling, and there is a flow script in the build that copies a Flow definition file into the built output under the name of the universal module bundle. So the package ships TypeScript declarations and Flow definitions, and the root has a Flow configuration file. The build itself runs a shell script, then a bundler, then the Flow copy step, so a build on Windows is not obviously a supported path even though the package installs there. None of this is a reason to avoid the package, and none of it affects what the tag does. It is, however, the answer to a question people ask about long-lived packages, which is how much of the original toolchain is still load bearing. Here, enough of it is that the build still produces a Flow definition for a type system most new projects do not use.
Two hand-written entry files at the root, and a release after three years
The build and release arrangement explains the shape of the repository. Development dependencies include a changesets tool and a changeset changelog integration, and the scripts include the three standard changesets commands for adding a changelog entry, versioning and publishing, so releases are driven by changesets rather than by hand-editing a version field. Continuous integration is a single configuration directory for one hosted service, and the tree also has a code owners file, a changelog and a contribution guide. Two smaller things are worth noting because they are easy to miss. The prepublish script is named prepublish rather than the modern hook for a package that builds itself on install, which npm deprecated in favour of prepare, so a git-based install of this package can behave differently from a registry install. And the test file is bundled into one file before the test runner executes it, which is why each of the three version targets can run the same suite after installing different dependencies. Now the release picture. The recent releases are 2.12.4 from 2021, 2.12.6 from October 2022, and 2.12.7 from 2026-06-17, which is the same day as the last push and the only one of the three tagged with a version prefix. A patch release after a gap of nearly four years is the signature of a package that is stable and rarely changing, which is appropriate for something whose entire job is parsing a document format that changes on someone else's schedule. The three-lane test matrix is the evidence that the maintainers still care about the edges, even when the version number does not move.
Editorial conclusion
Adopt graphql-tag if you write GraphQL in JavaScript or TypeScript and want queries that a linter can read, because the whole justification is static analysis and the redundant fragment spread is what makes it possible, and the parse cache gives you reference equality so you can skip re-sending an identical query. Do not adopt it expecting a client, a cache or a transport, because it produces an AST and stops there, and it ships only two things. Two things to check before you wire up the loader. The documented webpack configuration uses a loaders array, which is webpack 1 syntax, so you will need to translate it to a rules block before it works in a current build. And remember the runtime and build paths behave differently: at runtime the tag parses a string, while with the loader configured the imported value is already a pre-built AST, which changes what your code is bundling and how you test it.
Frequently asked questions
What does graphql-tag do and what do I need to install?
It provides a template literal tag that parses GraphQL query strings into the standard AST, plus a webpack loader. The reference graphql library is a peer dependency, so you install it alongside, and the only runtime dependency is tslib.
Why do I have to both interpolate a fragment and spread its name?
The README explains that while it seems redundant, the requirement makes static analysis by tools such as eslint-plugin-graphql possible. The fragment name has to appear as a literal inside the query for a linter to resolve it and check the fields.
Does graphql-tag cache parsed queries?
Yes. Previous parse results are kept in a simple dictionary, so the same query is not parsed twice, and because the same text returns the same object you can use the identity operator to compare queries. The cache is held for the life of the process with no documented eviction.
How do I configure the graphql-tag webpack loader?
Add a rule for the graphql and gql extensions that excludes node_modules and points at graphql-tag/loader. Note that the README's example uses a top-level loaders array, which is webpack 1 syntax, so it needs moving into a module rules block for current webpack versions.
Which TypeScript and graphql versions does graphql-tag test against?
Three combinations are defined: TypeScript 3.7 with graphql 15, TypeScript 4 with graphql 16.5, and TypeScript 6 with graphql 17. The test script runs the first two, so the TypeScript 6 and graphql 17 lane is defined but not part of the default run.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/apollographql-graphql-tag)