and graphql-schema-linter, combined properly
Invalid fields, mistyped arguments, and deprecated fields that keep getting used anyway: all of it can be caught right in the editor with GraphQL linting, instead of tripping over a cryptic server error at runtime.
Table of Contents
- 1. Why GraphQL linting is more than style checking
- 2. Two layers of GraphQL linting: schema vs. operations
- 3. Setting up eslint-plugin-graphql against a live schema
- 4. Common errors eslint-plugin-graphql catches at dev time
- 5. graphql-schema-linter: built-in rules overview
- 6. Writing your own lint rules
- 7. Integrating linting into CI/CD
- 8. Monorepos and multiple schemas: scaling the config
- 9. GraphQL linting tools compared
- 10. Summary
- 11. FAQ
1. Why GraphQL linting is more than style checking
GraphQL linting is often mistaken for plain style checking, but good GraphQL linting covers error classes that would otherwise only surface at runtime: a query requesting a field that doesn't even exist in the schema, a mutation missing a required argument, or a client still using a field already marked @deprecated. Without GraphQL linting, developers only notice such errors once the GraphQL server returns a validation error at runtime, in the worst case only in production.
The value of GraphQL linting comes from integrating it into the development workflow: as an IDE warning while typing, as a pre-commit hook before every commit, and as a CI check before every merge. Each of these three layers catches errors at a different point in time, and the earlier an error is caught, the cheaper it is to fix. The two established tools in the GraphQL ecosystem, eslint-plugin-graphql for client queries and graphql-schema-linter for the schema definition itself, together cover both sides.
2. Two layers of GraphQL linting: schema vs. operations
GraphQL linting splits into two independent layers that are often confused. The first layer is schema linting: it checks the SDL (schema definition language) file itself for consistency, naming conventions, missing descriptions and structural issues like circular input types. graphql-schema-linter is the reference tool here and works independently of a running server, directly on the .graphql or .graphqls file.
The second layer is operations linting, meaning checking client-side queries, mutations and fragments against a concrete schema. eslint-plugin-graphql handles this as an ESLint plugin that validates every query in .js, .ts or .graphql files against the actual schema definition. Both layers complement each other: a clean schema alone doesn't prevent broken client queries, and valid client queries alone say nothing about the quality of the schema.
# Install both layers of GraphQL linting
npm install --save-dev eslint eslint-plugin-graphql graphql
npm install --save-dev graphql-schema-linter
3. Setting up eslint-plugin-graphql against a live schema
eslint-plugin-graphql needs access to a schema, either as an introspection JSON export or as an SDL file. In the ESLint configuration, the plugin is enabled and the schema path is set; ESLint then checks every template-literal query tagged with gql or graphql against the schema. A call like products(fitler: {...}) with a mistyped argument name is flagged immediately as a lint error, long before the code is ever executed.
It's important to keep the schema updated regularly, otherwise eslint-plugin-graphql validates against a stale version and misses new breaking changes or reports false positives for newly added fields. An npm script that runs the introspection query against the development environment and writes the result to schema.json in the repository should be part of every build or pre-commit step.
// .eslintrc.js
module.exports = {
plugins: ['graphql'],
rules: {
'graphql/template-strings': ['error', {
env: 'apollo',
// Regenerate this file via a graphql-codegen introspection task
schemaJson: require('./schema.json'),
tagName: 'gql',
}],
},
};
4. Common errors eslint-plugin-graphql catches at dev time
The most common error class is using fields that don't exist, usually due to typos or a query written against an older version of the schema. eslint-plugin-graphql flags such fields with a precise error message that lists the expected type and the available fields, instead of the generic "Cannot query field" error the server would return at runtime.
A second important error class is using fields marked deprecated. Without GraphQL linting, such usage stays invisible until the field is actually removed and the query breaks. With the deprecation check enabled, a warning already appears in the editor as soon as a developer copies a deprecated field into a new query, which frequently happens through copy-pasting from an existing but equally outdated query.
5. graphql-schema-linter: built-in rules overview
graphql-schema-linter ships with more than 30 rules out of the box, roughly falling into three categories: naming rules (types-are-capitalized, fields-are-camel-cased, enum-values-all-caps), documentation rules (types-have-descriptions, fields-have-descriptions), and structural rules (relay-connection-types-spec, input-object-values-are-camel-cased). Each rule can be enabled or disabled individually, which is essential when rolling out GraphQL linting into an existing schema incrementally.
On an already production schema with hundreds of fields, enabling all rules at once produces a flood of error messages nobody can meaningfully work through. The pragmatic path is to enable only the most critical rules first, such as types-are-capitalized and enum-values-all-caps, deliberately ignore or fix existing violations, and add further rules gradually afterward.
{
"schemaPaths": ["./schema.graphql"],
"rules": [
"types-are-capitalized",
"fields-are-camel-cased",
"enum-values-all-caps",
"fields-have-descriptions",
"types-have-descriptions"
],
"ignoreExceptions": ["Product.legacy_sku"]
}
6. Writing your own lint rules
Both eslint-plugin-graphql and graphql-schema-linter allow custom rules for project-specific conventions that the built-in rule sets don't cover. A common case: boolean fields must start with is, has or can. graphql-schema-linter offers a custom rule API for this, operating on the schema's AST and checking node types like FieldDefinition through a visitor.
Custom rules are especially worth it for conventions that are documented in your own style guide but regularly get missed without automated checking. A simple custom rule takes only a few dozen lines of code and pays off quickly once more than one or two developers work on the schema at the same time.
// rules/boolean-field-prefix.js
module.exports = function booleanFieldPrefix(context) {
return {
FieldDefinition(node) {
const isBoolean = node.type.name && node.type.name.value === 'Boolean';
const name = node.name.value;
const hasPrefix = /^(is|has|can)[A-Z]/.test(name);
if (isBoolean && !hasPrefix) {
context.reportError(
context.createError(
`Boolean field "${name}" should start with is, has or can.`,
[node]
)
);
}
},
};
};
7. Integrating linting into CI/CD
GraphQL linting only reaches its full potential once it runs mandatorily in the CI pipeline instead of remaining optional in the local development environment. A GitHub Actions workflow that runs both ESLint with the graphql plugin and graphql-schema-linter on every pull request prevents broken queries or convention violations from ever reaching the main branch.
For fast feedback, a pre-commit hook via husky and lint-staged is worth adding too, checking only changed files instead of validating the entire schema on every commit. This keeps the feedback loop short while the full check in the CI pipeline continues to act as a safety net for the entire repository.
# .github/workflows/graphql-lint.yml
name: GraphQL Lint
on: [pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx eslint . --ext .js,.ts,.graphql
- run: npx graphql-schema-linter schema.graphql
8. Monorepos and multiple schemas: scaling the config
In monorepos with multiple GraphQL services or a federated schema, a single global lint configuration quickly becomes a bottleneck. A better approach is a base configuration at the root that project-specific .graphql-schema-linterrc files in each service directory extend, so shared rules are maintained centrally while individual services can add extra, domain-specific rules.
For eslint-plugin-graphql this means analogously providing a dedicated schema per frontend package, or the relevant slice of a federated schema, so linting errors reference the actually used sub-schema instead of the entire federated gateway schema, which is only partially relevant to any single frontend team anyway.
9. GraphQL linting tools compared
The table below classifies the common tools by their actual job, so the choice between them doesn't come down to a gut call.
| Tool | Checks | Strength | Limitation |
|---|---|---|---|
| eslint-plugin-graphql | Client queries against a schema | Direct IDE integration via ESLint | Not suited for the schema SDL itself |
| graphql-schema-linter | The schema SDL itself | Large rule library, custom rules | No client query validation |
| @graphql-eslint/eslint-plugin | Schema AND operations | One tool for both layers | Larger config, younger ecosystem |
| GraphQL Inspector | Schema diffs between versions | Detects breaking changes between commits | No classic style linting |
For new projects, @graphql-eslint/eslint-plugin is often the most pragmatic choice, since a single configuration covers both linting layers. For grown codebases already using eslint-plugin-graphql and graphql-schema-linter, migration usually only pays off when a bigger tooling update is already on the table.
Mironsoft
GraphQL tooling, CI pipelines and schema quality assurance
Catch broken queries right in the editor?
We set up GraphQL linting for schema and client queries, integrate it into your CI pipeline, and write project-specific custom rules for your naming conventions.
Linting setup
Configure eslint-plugin-graphql and graphql-schema-linter for production
Custom rules
Develop project-specific rules for your naming conventions
CI integration
Pre-commit hooks and GitHub Actions for fast, reliable feedback
10. Summary
GraphQL linting pays off because it catches error classes early that would otherwise only surface at runtime: wrong field names, missing required arguments, and continued use of deprecated fields. eslint-plugin-graphql handles checking client queries against a concrete schema, graphql-schema-linter checks the schema definition itself for naming conventions, documentation and structure. Both tools complement each other because they cover different layers of the GraphQL stack.
The biggest effect comes from integrating linting into the workflow: IDE warnings while typing, pre-commit hooks for fast local feedback, and a CI check as a mandatory safety net before every merge. Custom rules via both tools' rule APIs close the gap between a documented style guide and a convention that is actually enforced.
GraphQL Linting — The Essentials at a Glance
Two layers
eslint-plugin-graphql for client queries, graphql-schema-linter for the schema SDL. Both complement each other.
Custom rules
Both tools offer APIs for your own, project-specific rules beyond the built-in rule sets.
CI integration
GitHub Actions on every pull request, pre-commit hooks with husky and lint-staged for fast feedback.
Gradual rollout
On existing schemas, enable critical rules first, fix or ignore violations, then expand coverage.