This content originally appeared on Telerik Blogs and was authored by Hassan Djirdeh
GraphQL has changed how developers interact with data, but managing its complexity in Node.js can be a pain, especially with TypeScript. Let’s explore TypeGraphQL, a framework that aims to simplify the creation of GraphQL APIs in a Node.js and TypeScript setting.
In the modern landscape of web development, API design has become a cornerstone of application architecture. While GraphQL has revolutionized how developers interact with data, managing its complexity in a Node.js environment can sometimes be cumbersome, particularly when using TypeScript. This is where TypeGraphQL comes into play, a framework that aims to simplify the creation of GraphQL APIs in a Node.js setting.
In this article, we’ll delve into the fundamentals of TypeGraphQL, illustrating its benefits and usage.
Want to better understand what GraphQL is and how it differs from traditional REST APIs? Check out this article we’ve written before: GraphQL vs. REST—Which is Better for API Design?
TypeGraphQL
TypeGraphQL is a library designed to make building GraphQL APIs in TypeScript more efficient. It does this by leveraging decorators and TypeScript classes to define schemas and resolvers succinctly, allowing developers to write cleaner and more scalable code.
In a previous article we wrote on GraphQL Resolvers, we went through a simple example of fetching user data based on an ID. In this traditional setup, the GraphQL schema (SDL) and resolvers looked something like the following:
Schema:
type Query {
user(id: ID!): User
}
type User {
name: String!
email: String!
}
Resolver:
const resolvers = {
Query: {
user: (obj, args, context, info) => {
/*
fetch the user data from the database using
the id from args
*/
return database.getUserById(args.id);
},
},
};
The above features a schema and resolver for a user query. In the schema, a Query
type allows fetching a User
object by ID, which includes name
and email
fields. The resolver maps this query to a function that retrieves user data from a database using the provided ID, effectively bridging the GraphQL schema to the database.
Let’s see how this traditional approach translates into a TypeGraphQL framework, enhancing type safety and reducing boilerplate.
TypeGraphQL Schema and Resolver
First, we define the GraphQL types using TypeScript classes and decorators provided by TypeGraphQL, like ObjectType
, Field
and ID
:
import { ObjectType, Field, ID } from "type-graphql";
@ObjectType()
class User {
@Field((type) => ID)
id: string;
@Field()
name: string;
@Field()
email: string;
}
In the above example, we define the GraphQL object type for a User
using TypeScript classes, which enhances type safety and maintainability. Each field of the User
object is defined with a @Field
decorator, which describes the type of data (such as ID
, String
, etc.) and whether it is nullable or not. This approach integrates the GraphQL schema directly into the TypeScript environment, so the data types used are consistent across the API.
Next, we define the resolver class which will handle the fetching of user data. We’ll use the @Resolver
decorator from TypeGraphQL to bind our class to the specified GraphQL type, which in this case is User
:
import { Resolver, Query, Arg } from "type-graphql";
import { User } from "./User";
@Resolver((of) => User)
class UserResolver {
@Query((returns) => User, { nullable: true })
async user(@Arg("id") id: string): Promise<User | undefined> {
// Use the database service to fetch user data by ID
return await database.getUserById(id);
}
}
In the above example, we implement a resolver class to handle queries for fetching user data. The @Resolver
decorator indicates that this class will resolve fields for the User
type. The @Query
decorator is used to define a query operation within the GraphQL API. The user
function takes an id
argument marked with the @Arg
decorator and returns a User
object or undefined
if no user is found with that ID. This setup not only simplifies the code but also ensures that each component of the API is strongly typed and adheres to the schema definitions.
To build the schema, we can use the buildSchema()
function provided by TypeGraphQL.
import { buildSchema } from "type-graphql";
import { UserResolver } from "./UserResolver";
async function createSchema() {
return await buildSchema({
resolvers: [UserResolver],
});
}
In the above example, buildSchema()
is configured with UserResolver
, integrating all the logic we defined earlier and creates an executable schema from our type and resolver definitions. Finally, we’ll need to actually run the async function:
import { buildSchema } from "type-graphql";
import { UserResolver } from "./UserResolver";
async function createSchema() {
return await buildSchema({
resolvers: [UserResolver],
});
}
createSchema();
With the schema now built, the GraphQL endpoint can be created and served using tools like Apollo Server or graphql-yoga, showcasing how TypeGraphQL facilitates the integration of TypeScript with GraphQL. The shift from traditional GraphQL setup to TypeGraphQL introduces a more structured and scalable approach to building GraphQL APIs, leveraging the full power of TypeScript’s static typing system.
The above only touches the surface of what can be done with TypeGraphQL, and there’s a lot more to explore. Below, we’ll go through one core feature TypeGraphQL supports well—authorization.
Authorization
In TypeGraphQL, the @Authorized decorator is used to implement authorization controls directly within schema definitions. This powerful feature allows us to specify which roles can access specific fields or execute queries and mutations.
Let’s expand the previous User
example to include authorization checks that only authorized users can access certain user details. First, we’ll modify the UserResolver
to include methods that are protected with the @Authorized
decorator, specifying which roles are allowed to access them.
import { Resolver, Query, Arg, Mutation, Authorized } from "type-graphql";
import { User, UserInput } from "./User";
@Resolver((of) => User)
class UserResolver {
// Public query accessible by anyone
@Query((returns) => User, { nullable: true })
async user(@Arg("id") id: string): Promise<User | undefined> {
return await database.getUserById(id);
}
// Restricted query only accessible by admins
@Authorized("ADMIN")
@Query((returns) => [User])
async allUsers(): Promise<User[]> {
return await database.getAllUsers();
}
// Mutation to update user, restricted to logged-in users
@Authorized()
@Mutation((returns) => User)
async updateUser(@Arg("userData") userData: UserInput): Promise<User> {
return await userService.update(userData);
}
}
In the code example above, we enhanced the UserResolver
by integrating role-based access control. Queries and mutations are protected using the @Authorized
decorator, specifying role requirements directly in the GraphQL schema definition. This setup restricts access based on user roles, so that only appropriately authorized users can perform certain actions, such as fetching all users or updating user details.
Next, we need to create an auth checker function, the function that defines the logic to verify if the current user’s roles match the roles required by the @Authorized
decorator.
import { AuthChecker } from "type-graphql";
import { Context } from "./Context";
export const customAuthChecker: AuthChecker<MyContext> = (
{ context },
roles
) => {
// Ensure there is a user in the context
if (!context.user) return false;
// If no specific roles are required, allow any authenticated user
if (!roles.length) return true;
// Check if the user’s roles include any of the required roles
return roles.some((role) => context.user.roles.includes(role));
};
The above example auth checker function checks:
- If a user is present in the context.
- If no roles are specified, it allows access to any authenticated user.
- If specific roles are specified, it checks if the user has any of those roles.
Finally, we’ll need to integrate this auth checker function when we build our GraphQL schema with TypeGraphQL. This step involves registering the custom auth checker function with the schema.
import { buildSchema } from "type-graphql";
import { UserResolver } from "./UserResolver";
import { customAuthChecker } from "./auth/custom-auth-checker";
async function createSchema() {
return await buildSchema({
resolvers: [UserResolver],
authChecker: customAuthChecker,
});
}
By following the steps, we would have successfully integrated role-based authorization into our TypeGraphQL API. This setup not only secures our API but also makes it so that only authorized users can access sensitive operations and data.
Wrap-up
In this article, we’ve introduced some of the core concepts of what TypeGraphQL has to offer. Beyond the basics of defining schemas and resolvers, TypeGraphQL comes packed with features such as dependency injection, validation, inheritance and middleware, which enhance its capabilities. Furthermore, TypeGraphQL supports more advanced GraphQL capabilities like enums, subscriptions and directives.
TypeGraphQL’s integration of these capabilities into the TypeScript ecosystem allows developers to produce secure, scalable and maintainable APIs efficiently. For more details, be sure to check out the official TypeGraphQL documentation!
This content originally appeared on Telerik Blogs and was authored by Hassan Djirdeh
Hassan Djirdeh | Sciencx (2024-07-26T09:14:09+00:00) Introduction to TypeGraphQL. Retrieved from https://www.scien.cx/2024/07/26/introduction-to-typegraphql/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.