loading...
Cover image for Announcing TypeGraphQL 1.0 🚀

Announcing TypeGraphQL 1.0 🚀

michallytek profile image Michał Lytek ・6 min read

It's finally happening! Over two years after the initial announcement, TypeGraphQL is now ready for its first stable release - v1.0.0 🎉

It was a really long journey that started in 31st of January 2018 with releasing v0.1.0 and which contained 650+ commits, 85+ merged PRs and 4.9k+ stars on GitHub.

If you haven't heard about TypeGraphQL yet, it's a modern framework for creating GraphQL APIs with TypeScript in Node.js, using only classes and decorators.
To get know why "GraphQL + TypeScript = TypeGraphQL", please check out the initial announcement article before you continue reading this post.

This post is focused mostly on presenting new features and describing changes in the newest stable release. Well, then, without further ado... let's take a look what the TypeGraphQL 1.0 brings us!

Performance

One of the most important things which is also often neglected by developers - the performance. One of the key focus area for the 1.0 release was making it blazingly fast ⚡

TypeGraphQL is basically an abstraction layer built on top of the reference GraphQL implementation for JavaScript - graphql-js. To measure the overhead of the abstraction, a few demo examples were made to compare it against the "bare metal" - using raw graphql-js library.

It turned out that in the most demanding cases like returning an array of 25 000 nested objects, the old version 0.17 was even about 5 times slower!

library execution time
TypeGraphQL v0.17 1253.28 ms
graphql-js 265.52 ms

After profiling the code and finding all the root causes (like always using async execution path), the overhead was reduced from 500% to just 17% in v1.0.0! By using simpleResolvers it can be reduced even further, up to 13%:

execution time
graphql-js 265.52 ms
TypeGraphQL v1.0 310.36 ms
with "simpleResolvers" 299.61 ms
with a global middleware 1267.82 ms

Such small overhead is much easier to accept than the initial 500%!
More info about how to enable the performance optimizations in the more complex cases can be found in the docs 📖.

Schema isolation

This is another feature that is not visible from the first sight but gives new possibilities like splitting the schema to public and private ones 👀

In 0.17.x and before, the schema was built from all the metadata collected by evaluating the TypeGraphQL decorators. The drawback of this approach was the schema leaks - every subsequent calls of buildSchema was returning the same schema which was combined from all the types and resolvers that could be find in the metadata storage.

In TypeGraphQL 1.0 it's no longer true!
The schemas are now isolated which means that the buildSchema call takes the resolvers array from options and emit only the queries, mutation and types that are related to those resolvers.

const firstSchema = await buildSchema({
  resolvers: [FirstResolver],
});
const secondSchema = await buildSchema({
  resolvers: [SecondResolver],
});

So just by modifying the resolvers option we can have different sets of operations exposed in the GraphQL schemas!
Proper isolation also makes serverless development easier as it allows to get rid of the "Schema must contain uniquely named types" errors and others.

Directives and extensions

This two new features are two complementary ways to put some metadata about the schema items.

GraphQL directives though the syntax might remind the TS decorators, as "a directive is an identifier preceded by a @ character", but in fact, they are a purely Schema Definition Language feature. Apart from the metadata capabilities, they can also modify the schema and e.g. generate the connection type for pagination purposes. Basically, the looks like this:

type Query {
  foobar: String! @auth(requires: USER) 
}

To apply them, we just need to put the @Directive decorator above and supply the string argument, e.g.:

@Resolver()
class FooBarResolver {
  @Directive("@auth(requires: USER)")
  @Query()
  foobar(): string {
    return "foobar";
  }
}

However, on the other side we have the GraphQL extensions which are the JS way to achieve the same goal. It's the recommended way of putting the metadata about the types when applying some custom logic.

To declare the extensions for type or selected field, we need to use @Extensions decorator, e.g.:

@ObjectType()
class Foo {
  @Extensions({ roles: [Role.User] })
  @Field()
  bar: string;
}

We can then read that metadata in the resolvers or middlewares, just by exploring the GraphQLResolveInfo object, e.g.:

export const ExtensionsMiddleware: MiddlewareFn = async ({ info }, next) => {
  const { extensions } = info.parentType.getFields()[info.fieldName];
  console.log(extensions?.roles); // log the metadata
  return next();
};

More info about directives and extensions features can be found in docs 📖

Resolvers and arguments for interface fields

The last thing that was preventing TypeGraphQL from being fully GraphQL compliant thus blocking the 1.0 release - an ability to provide interface fields resolvers implementations and declare its arguments.

Basically, we can define resolvers for the interface fields using the same syntax we would use in case of the @ObjectType, e.g.:

@InterfaceType()
abstract class IPerson {
  @Field()
  avatar(@Arg("size") size: number): string {
    return `http://i.pravatar.cc/${size}`;
  }
}

...with only a few exceptions for cases like abstract methods and inheritance, which you can read about in the docs.

More descriptive errors messages

One of the most irritating issues for newcomers were the laconic error messages that haven't provided enough info to easily find the mistakes in the code.

Messages like "Cannot determine GraphQL input type for users" or even the a generic "Generating schema error" were clearly not helpful enough while searching for the place where the flaws were located.

Now, when the error occurs, it is broadly explained, why it happened and what could we do to fix that, e.g.:

Unable to infer GraphQL type from TypeScript reflection system.
  You need to provide explicit type for argument named 'filter'
  of 'getUsers' of 'UserResolver' class.

or:

Some errors occurred while generating GraphQL schema:
  Interface field 'IUser.accountBalance' expects type 'String!'
  but 'Student.accountBalance' is of type 'Float'

That should allow developers to safe tons of time and really speed up the development 🏎

Transforming nested inputs and arrays

In the previous releases, an instance of the input type class was created only on the first level of inputs nesting.
So, in cases like this:

@InputType()
class SampleInput {
  @Field()
  sampleStringField: string;

  @Field()
  nestedField: SomeNestedInput;
}

@Resolver()
class SampleResolver {
  @Query()
  sampleQuery(@Arg("input") input: SampleInput): boolean {
    return input.nestedField instanceof SomeNestedInput;
  }
}

the nestedField property of input was just a plain Object, not an instance of the SomeNestedInput class. That behavior was producing some unwanted issues, including limited support for inputs and args validation.

Since 1.0 release, it's no longer an issue and all the nested args and inputs are properly transformed to the corresponding input type classes instances, even including deeply nested arrays 💪

One more thing...

The 1.0 release is not our last word! We have plenty of feature requests from the community and tons of our ideas to implement, so stay tuned and wait for more! 💪

Also, please keep in mind that TypeGraphQL is an MIT-licensed open source project. It doesn't have a large company that sits behind - its ongoing development is possible only thanks to the support by the community.

GitHub Sponsors

Open Collective

If you fell in love with TypeGraphQL, please consider supporting our efforts and help it grow, especially if you are using it commercially - just to ensure that the project which your product relies on is actively maintained and improved.

GitHub logo MichalLytek / type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!

logo

TypeGraphQL

npm version Build Status codecov dependencies install size gitter

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!

https://typegraphql.com/

Introduction

TypeGraphQL makes developing GraphQL APIs an enjoyable process, i.e. by defining the schema using only classes and a bit of decorator magic.

So, to create types like object type or input type, we use a kind of DTO classes For example, to declare Recipe type we simply create a class and annotate it with decorators:

@ObjectType()
class Recipe {
  @Field(type => ID)
  id: string;
  @Field()
  title: string;
  @Field(type => [Rate])
  ratings: Rate[];

  @Field({ nullable: true })
  averageRating?: number;
}

And we get the corresponding part of the schema in SDL:

type Recipe {
  id: ID!
  title: String!
  ratings: [Rate

Discussion

pic
Editor guide
Collapse
idoshamun profile image
Ido Shamun

Awesome work Michal! I used TypeGraphQL for a while and it's was super fun compared to the bare GraphQL and especially combined with TypeORM. Eventually I decided to drop TypeGraphQL because of the performance. This was before v1.0. Now I see that you improved the performance dramatically but still my feeling is that if I use several features of TypeGraphQL (e.g global resolver), I might have performance issues. Are you planning tackling this in the future?

Collapse
michallytek profile image
Michał Lytek Author

Are you planning tackling this in the future?

Yes, I've started working on TypeGraphQL vNext (2.0?) which is a complete rewrite of the internals to fulfill the minimal overhead goal, as well as to provide capabilities for implementing additional features or integrations easily, using the plugins concept. No ETA yet 😉

Collapse
idoshamun profile image
Ido Shamun

Looking forward! Please consider an option to use TypeGraphQL just for schema generation with any runtime overhead

Thread Thread
michallytek profile image
Michał Lytek Author

There always be some overhead (because of different method signatures while using @Args etc.) but will be as minimal as possible 😉

Thread Thread
idoshamun profile image
Ido Shamun

Yes, I agree. But it seems you are closing the gap!
Btw, why do you use grpahql-jit and not apollo?

Thread Thread
michallytek profile image
Michał Lytek Author

You can use jit with apollo but apollo is super slow as a http server:

apollo-server-express                 1238.6
apollo-server-fastify                 1383.6
apollo-server-fastify + graphql-jit   1740.6
fastify-gql + graphql-jit             5437.2
Enter fullscreen mode Exit fullscreen mode

github.com/benawad/node-graphql-be...

Thread Thread
idoshamun profile image
Ido Shamun

My combination is fastify with apollo. I use their Automatic persisted queries.
The table still shows that it should be pretty slow compared to jit. I need to check it out

Collapse
joaomelo profile image
joão melo

Hi, Michal. Beautiful work. I love the library. Congrats, and thank you!

Collapse
arslnb profile image
Arsalan Bashir

Hey Michal! Nice work! I was wondering what your thoughts are on using TypeGraphQL v1.0 in production and where I might find notes on upgrade instructions etc, if any :)

Collapse
michallytek profile image
Michał Lytek Author

There shouldn't be any troubles on using TypeGraphQL v1.0 in production.
Feel free to ask on Gitter if you find some issues while upgrading 😉

Collapse
2color profile image
Daniel Norman

Congratulations Michał!

Collapse
dnature profile image
Divine Hycenth

Decorators and Classes ++