# graphql-compose and the type registry behind programmatic schemas

> graphql-compose is a TypeScript library that provides a type registry with methods for programmatic GraphQL schema construction, so a schema can be assembled, edited and extended in code rather than written in SDL and then patched. It ships generators for Mongoose, Elasticsearch, JSON and AWS, a set of resolver utilities, and it is published under MIT with a semantic release pipeline.

**graphql-compose/graphql-compose** — Toolkit for generating complex GraphQL Schemas on Node.js

- Repository: https://github.com/graphql-compose/graphql-compose
- Website: https://graphql-compose.github.io/
- Stars: 1,212 · Forks: 74
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/graphql-compose-graphql-compose

## A registry, not a schema file

graphql-compose provides a type registry with a bunch of methods for programmatic schema construction. The stated purpose is blunt: if you want to write your own GraphQL schema generator, this is a good instrument for it.

That framing matters, because it is not a GraphQL server and it does not replace one. The schema it produces is passed on to express-graphql, apollo-server or graphql-yoga, in exactly the way the README's example ends with a call to schemaComposer.buildSchema().

The capabilities the project lists are all about editing a type registry rather than describing a schema. There are methods for editing GraphQL output and input types, adding and removing fields, arguments and interfaces. There is a converter from an OutputType to an InputType, which is the operation you need constantly in a GraphQL API and which is tedious to do by hand. There is a projection parser from the AST, and a GraphQL schema language for defining simple types. Two extra scalar types are added, Date and Json.

The audience is a team building a GraphQL API whose schema is partly or wholly derived from something else: a database model, a REST service, a set of microservices. The classic version of that problem is a schema file that drifts from the models, and this library is a way to make the schema a function of the models instead.

The package is published on npm under the name graphql-compose, and the install is a single dependency:

```bash
npm install graphql-compose
```

It is written in TypeScript and ships type declarations with the compiled output, with main pointing at lib/index.js and types at lib/index.d.ts.

## Resolvers as objects you can find and reuse

The feature the README puts first after type editing is the introduction of Resolvers, described as the named GraphQL fieldConfigs, which can be used for finding, updating and removing records. The plural matters, because the naming implies a collection rather than a single function.

In practice a resolver is registered on a type with a name, a kind, a set of arguments and a resolve function. The example registers findMany on the City type, with a filter argument, a limit argument that defaults to 20 and a skip argument, and a resolve that reads from a city database in the context.

The payoff is that a resolver becomes an addressable object. CityTC.getResolver('findMany') returns it, and it can be attached to the schema as a field, which is what the example does when it adds a cities field to the root Query type. The same is done for mutations, with createCity, updateCity and removeCity resolvers attached to the root Mutation type.

The second half of the resolver idea is relations. The README says the library provides an easy way for creating relations between types via Resolvers, and the example shows a country field added to the City type pointing at another type. A relation is a field whose value comes from another record, and having it as a resolver rather than a bare type reference is what makes it possible to load it lazily, with the projection mechanism deciding what the database is asked for.

The composition at the end of the example is the pattern to copy. The city resolvers are attached to Query and Mutation, and a small helper wraps the removeCity resolver so that it throws unless the context says the caller is an admin. Access control lives in the resolver, where the data access already is.

## Three ways to define a type, and one shorthand for fields

The README demonstrates the ambiguity of type definition, and the demonstration is instructive because it covers three approaches that end up in the same registry.

A type can be created from the GraphQL schema language as a string, which is the SDL form most people already know. It can be created from a config object with a name and a fields map, which is where the library's own extensions live. And a config object's fields can themselves be strings, a small GraphQL schema language snippet, or a thunk that returns another type, which is the escape hatch for the hoisting problem when two types reference each other.

Once a type exists, fields are added with addFields, and the field definition itself has several accepted shapes. A field can be a bare type, a string, a standard GraphQL field config, or an extended config with extra keys.

That last shape is where the library earns its keep. The example's currentLocalTime field declares a type of Date, a resolve function that formats the current time in the source's timezone, and a projection of tz set to true, with a comment explaining that this loads tz from the database when only the localTime field is requested. A standard GraphQL field config has no place to express that, so a library that only understood standard configs could not offer it.

The example also shows the shorthand for a type with no resolver, counter set to Int, the list forms, list0 through list4, showing that a list type can be written as a bracketed string, as a string, as an array of type instances or as an inline type definition.

## Editing a schema after the fact is the point of the registry

The opening description says the library allows not only to extend types but also to remove fields, interfaces and args, and that capability is what a type registry buys you over a schema definition.

The example shows the two edit operations in one line each. CityTC.removeField('tz') takes a field out of the type, and CityTC.extendField('name', { description: 'City name' }) adds a description to a field that already exists. Both are trivial with a registry and awkward without one, because without one you have to go back to the SDL, change it, and accept that whatever generated it will regenerate the old version.

That is the actual problem the library solves in practice. A schema built from a Mongoose model or an Elasticsearch mapping comes with fields you do not want exposed, and the usual choices are to fork the generator, to write a post-processing pass over the generated SDL, or to accept the extra fields. With a registry the removal is a statement in your code, next to the rest of the schema, and it survives regeneration.

The same applies to the root types. schemaComposer.Query.addFields and schemaComposer.Mutation.addFields attach fields to the entry points of the schema, and the example spreads an adminAccess helper into the mutation fields to attach one protected field alongside the public ones.

A caveat worth stating: the removal happens on the type as the library knows it, which is not the same as removing the field from the database. The registry removes the field from the GraphQL schema, and it is up to your resolvers to stop selecting it.

## Projection asks the source for less

The projection feature is the one that connects the schema layer to the database layer, and it is worth understanding because it changes the shape of the query rather than just the response.

The README lists a projection parser from AST among the features, and the field definition example uses it: the currentLocalTime field declares projection: { tz: true }, with the comment that it loads tz from the database when only the localTime field is requested. The same pattern appears in the resolver, where the filter and the limit and the skip arguments are passed straight to a findMany call on a city database.

What that adds up to is that the library can work out which fields the client actually asked for and use that to decide what to fetch. A client that asks only for the name of a city does not need the timezone, the population or the country code loaded from the database to compute a formatted local time. With the projection in place, the field resolver declares what it needs, tz, and the query layer is free to fetch that and nothing else.

This matters most with the generators, where a model can have fifty fields and a client typically wants three. Without projection, every query loads the record as the ORM model defines it, and the response is trimmed afterwards. With it, the data access is trimmed too.

The limit of the claim is that the projection works with resolvers that know how to use it, which is the case for the built-in generators and their findMany style resolvers, and is not automatic for a hand written resolver that ignores the projection argument.

## The plugin ecosystem is where the leverage is

The README splits the satellite packages into two groups, and the distinction is useful.

The type generators are declarative generators and plugins built on top of graphql-compose that take some ORMs, schema definitions and create GraphQL models from them or modify existing GraphQL types. graphql-compose-json generates a GraphQL type from JSON, which the README calls a good helper for wrapping REST APIs. graphql-compose-mongoose generates types from Mongoose models, that is MongoDB, with resolvers. graphql-compose-elasticsearch generates types from Elastic mappings and provides an ElasticSearch REST API proxy through GraphQL. graphql-compose-aws exposes the AWS Cloud API through GraphQL.

The utility plugins address problems that show up once a schema exists. graphql-compose-relay reassembles types with Relay specific things, the Node type and interface, globalId and clientMutationId. graphql-compose-connection generates a connection resolver from findMany and count resolvers, which is the shape Relay clients expect for paginated lists. graphql-compose-dataloader adds DataLoader to the resolvers, which is the batching layer that stops the N plus 1 problem.

The DataLoader plugin deserves a note, because it points at a structural limit of the model. The resolver approach makes each field's data source explicit, which is what makes relations and projections possible, and it also means every field can issue its own query. A nested selection can turn into a query per object unless something batches them, and that is what the DataLoader plugin is for.

The library also has live demos, a GraphQL server with nine models and fourteen files at roughly 750 lines, and a Relay client with eight pages and 47 files at roughly 3000 lines, which together show what the pieces look like assembled.

## MIT, semantic release, and four GraphQL majors in the peer range

The licence is MIT, with the text in LICENSE.md, and the project is funded through Open Collective, with backer and sponsor badges in the README.

The versioning is worth a look because it is unusual for a library at version nine. The version field in package.json reads 0.0.0-semantically-released, and the release script is semantic-release, so the published numbers come from commit messages rather than from a hand-edited field. The recent tags are v9.2.3 on 2026-10-13, v9.2.2 on 2026-09-18 and v9.2.1 on 2026-09-01, which is a patch-level release every couple of weeks.

The peer dependency is the other thing to read carefully. The package declares graphql as a peer dependency with a range of ^14.2.0, || ^15.0.0, || ^16.0.0, || ^17.0.0, so the library is expected to work with four major versions of GraphQL. The development environment uses graphql 17.0.2, and there is a single runtime dependency, graphql-type-json, which supplies the JSON scalar.

The toolchain around it is current. The build is rimraf lib followed by tsc with the build tsconfig, the tests run under jest with the swc transform, linting is oxlint with the correctness rules enabled and no-unused-vars disabled, formatting is prettier, and the test script chains coverage, lint, format check and a type check together. TypeScript is at 7.0.2 in the development dependencies and yarn 1.22.22 is the declared package manager, which is worth knowing because it is the older major of the package manager and the lockfile in the repository is a yarn.lock.

The repository is not archived and the last push was on 2026-10-13, the same day as the newest tag.

## Conclusion

Adopt graphql-compose if you generate part of your GraphQL schema from data sources, or if you need to edit a schema in code, since removing a field from a type that a library produced is otherwise awkward. Do not adopt it for a small hand written schema, where the SDL file and a plain graphql library are simpler, and do not expect it to replace the server, since the built schema is passed to express-graphql, apollo-server or graphql-yoga. Verify three things first: that your graphql version satisfies the peer dependency range, which spans versions 14.2 through 17, that the generator plugin for your data source exists and fits your schema conventions, and that the resolver wrapping pattern shown in the README, which enforces authorisation in the resolver rather than in the schema, matches how your team handles access control. The last push was on 2026-10-13 and the newest release is v9.2.3 from the same day.

## FAQ

### What is graphql-compose and what does it replace?

It is a TypeScript type registry with methods for programmatic GraphQL schema construction, letting you add and remove fields, arguments and interfaces rather than only writing SDL. It does not replace the server, since the built schema is passed to express-graphql, apollo-server or graphql-yoga.

### How do I install graphql-compose?

The package is published on npm as graphql-compose and is imported as import { schemaComposer } from 'graphql-compose'. It declares graphql as a peer dependency with a range of ^14.2.0 || ^15.0.0 || ^16.0.0 || ^17.0.0, and its only runtime dependency is graphql-type-json.

### What are Resolvers in graphql-compose?

They are the named GraphQL fieldConfigs used for finding, updating and removing records, registered on a type with a kind, arguments and a resolve function. A resolver can be attached to the schema with getResolver, and relations between types are created through them.

### What does the projection option do in graphql-compose?

The library parses the projection from the AST so that a field resolver can declare what it needs from the source, for example projection: { tz: true } to load the timezone only when that field is requested. A hand written resolver that ignores the projection will not benefit from it.

### Which plugins exist for graphql-compose?

Type generators include graphql-compose-json for JSON and REST APIs, graphql-compose-mongoose for MongoDB models, graphql-compose-elasticsearch for Elastic mappings and graphql-compose-aws for the AWS Cloud API. Utility plugins cover Relay, connection resolvers and DataLoader.

### How do I build and test graphql-compose?

The build script runs rimraf lib and then tsc with the build tsconfig, and the test script chains coverage, lint, format check and a type check. Releases are produced by semantic-release, and the last push was on 2026-10-13 with the newest tag v9.2.3.

## Sources

- [graphql-compose/graphql-compose on GitHub](https://github.com/graphql-compose/graphql-compose)
- [License: MIT](https://github.com/graphql-compose/graphql-compose/blob/master/LICENSE)
- [Project website](https://graphql-compose.github.io/)
- [README](https://github.com/graphql-compose/graphql-compose/blob/master/README.md)
- [Releases](https://github.com/graphql-compose/graphql-compose/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/graphql-compose-graphql-compose
