TypeGraphQL v1.0
On August 19, the TypeGraphQL framework was released, which simplifies working with GraphQL in Typescript. For two and a half years, the project has acquired a solid community and support from several companies and is confidently gaining popularity. After over 650 commits, he has over 5,000 stars and 400 forks on github, the fruit of the hard work of Polish developer Michal Litek. In version 1.0, performance improved significantly, schemas got isolation and got rid of the previous redundancy, two major features appeared - directives and extensions, the framework was brought to full compatibility with GraphQL.
What is this framework for?
Michal, referring to his experience with bare GraphQL, calls the development process "painful" due to the redundancy and complexity of modifying existing code:
It doesn't sound very practical, and with this approach, the main problem is the redundancy of the code, which makes it difficult to synchronize all parameters when writing it and adds risks when changing. To add a new field to our entity, we have to iterate over all the files: change the entity class, then change the schema part and the interface. It's the same with input data or arguments, it's easy to forget to update one element or make a mistake in one type.
To combat redundancy and automate all this manual labor, TypeGraphQL was created. It is based on the idea of โโstoring all information in one place, describing the data schema through classes and decorators. The framework also takes on the manual work of dependency injection, data validation and authorization, unloading the developer.
Principle of operation
Let's look at how TypeGraphQL works using the GraphQL API for the recipe database as an example.
This is what the schema looks like in SDL:
type Recipe {
id: ID!
title: String!
description: String
creationDate: Date!
ingredients: [String!]!
}
Let's rewrite it as a Recipe class:
class Recipe {
id: string;
title: string;
description?: string;
creationDate: Date;
ingredients: string[];
}
Let's equip the class and properties with decorators:
@ObjectType()
class Recipe {
@Field(type => ID)
id: string;
@Field()
title: string;
@Field({ nullable: true })
description?: string;
@Field()
creationDate: Date;
@Field(type => [String])
ingredients: string[];
}
Detailed rules for describing fields and types in the corresponding section of the documentation
Then we will describe the usual CRUD queries and mutations. To do this, create a RecipeResolver controller with the RecipeService passed to the constructor:
@Resolver(Recipe)
class RecipeResolver {
constructor(private recipeService: RecipeService) {}
@Query(returns => Recipe)
async recipe(@Arg("id") id: string) {
const recipe = await this.recipeService.findById(id);
if (recipe === undefined) {
throw new RecipeNotFoundError(id);
}
return recipe;
}
@Query(returns => [Recipe])
recipes(@Args() { skip, take }: RecipesArgs) {
return this.recipeService.findAll({ skip, take });
}
@Mutation(returns => Recipe)
@Authorized()
addRecipe(
@Arg("newRecipeData") newRecipeData: NewRecipeInput,
@Ctx("user") user: User,
): Promise<Recipe> {
return this.recipeService.addNew({ data: newRecipeData, user });
}
@Mutation(returns => Boolean)
@Authorized(Roles.Admin)
async removeRecipe(@Arg("id") id: string) {
try {
await this.recipeService.removeById(id);
return true;
} catch {
return false;
}
}
}
Here, the @Authorized () decorator is used to restrict access to unauthorized (or insufficiently privileged) users. You can read more about authorization in the documentation .
It's time to add NewRecipeInput and RecipesArgs:
@InputType()
class NewRecipeInput {
@Field()
@MaxLength(30)
title: string;
@Field({ nullable: true })
@Length(30, 255)
description?: string;
@Field(type => [String])
@ArrayMaxSize(30)
ingredients: string[];
}
@ArgsType()
class RecipesArgs {
@Field(type => Int, { nullable: true })
@Min(0)
skip: number = 0;
@Field(type => Int, { nullable: true })
@Min(1) @Max(50)
take: number = 25;
}
Length, Minand @ArrayMaxSize are decorators from the validator class that do field validation automatically.
The last step is actually assembling the circuit. This is done by the buildSchema function:
const schema = await buildSchema({
resolvers: [RecipeResolver]
});
And that's it! We now have a fully working GraphQL schema. In compiled form, it looks like this:
type Recipe {
id: ID!
title: String!
description: String
creationDate: Date!
ingredients: [String!]!
}
input NewRecipeInput {
title: String!
description: String
ingredients: [String!]!
}
type Query {
recipe(id: ID!): Recipe
recipes(skip: Int, take: Int): [Recipe!]!
}
type Mutation {
addRecipe(newRecipeData: NewRecipeInput!): Recipe!
removeRecipe(id: ID!): Boolean!
}
This is an example of basic functionality, in fact, TypeGraphQL can use a bunch of other tools from the TS arsenal. You have already seen links to documentation :)
What's new in 1.0
Let's take a quick look at the main changes in the release:
Performance
TypeGraphQL is essentially an additional layer of abstraction over the graphql-js library, and it will always run slower than it. But now, compared to version 0.17, on a sample of 25,000 nested objects, the framework adds 30 times less overhead - from 500% to 17% with the possibility of accelerating up to 13%. Some non-trivial optimization methods are described in the documentation .
Isolating circuits
In older versions, the schema was built from all the metadata obtained from decorators. Each subsequent call to buildSchema returned the same schema, built from all the metadata available in the store. Now the schemas are isolated and buildSchema issues only those requests that are directly related to the given parameters. That is, by changing only the resolvers parameter, we will perform different operations on GraphQL schemas.
Directives and extensions
There are two ways to add metadata to schema elements: GraphQL directives are part of the SDL and can be declared directly in the schema. They can also change it and perform specific operations, for example, generate a connection type for pagination. They are applied using the @Directive and @Extensions decorators and differ in their approach to schema construction. Documentation Directives , the Extensions documentation .
Converters and Arguments for Interface Fields
The last frontier of full compatibility with GraphQL lay here. You can now define converters for interface fields using the @ObjectType syntax:
@InterfaceType()
abstract class IPerson {
@Field()
avatar(@Arg("size") size: number): string {
return `http://i.pravatar.cc/${size}`;
}
}
A few exceptions are described here .
Converting nested inputs and arrays
In previous versions, an instance of the input class was created only at the first nesting level. This created problems and bugs with their validation. Fixed.
Conclusion
During the entire development period, the project remained open to ideas and criticism, open source and incendiary. 99% of the code was written by Michal Litek himself, but the community also made a huge contribution to the development of TypeGraphQL. Now, with increasing popularity and financial support, it can become a true standard in its field. Michal's Github Doki Twitter
website