GraphQL has gained quite a lot of popularity lately. And no wonder, because it is an interesting approach to querying data. Therefore, it is definitely worth looking into. At its core, GraphQL is a query language with a runtime that can execute them. Although it is an alternative to REST, it can work alongside it.
Queries
With REST, we have the concept of multiple endpoints. To retrieve the resource, we need to perform a GET request to this specific URL.
1{
2 "items": [
3 {
4 "id": 10,
5 "title": "Hello world!",
6 "paragraphs": [
7 "Lorem ipsum",
8 "Dolor sit amet"
9 ]
10 },
11 // ...
12 ]
13}With GraphQL, we approach the above task differently. First, we need to define a schema. We define it in a file with the .gql extension. Our GraphQL application needs to define a set of possible data our users can query. Our API validates incoming queries and executes them against the schema.
The most fundamental part of a schema is the object type. It represents what objects our users can fetch from our service and what fields they have.
1type Post {
2 id: Int!,
3 title: String!,
4 paragraphs: [String!]!
5}Above, we define a simple object type that contains three fields. All of them are non-nullable, and we indicate it with the exclamation mark. Because of that, every time our users query posts, each post is guaranteed to contain the id, the title, and the paragraphs.
Instead of calling a specific endpoint that manages posts, we typically have a single endpoint for managing all of our resources. We need to send a query to it to describe what data do we need.
First, we need to define our query in a similar manner that we’ve created our object type.
1type Query {
2 posts(): [Post!]!
3}Once that’s implemented, we can perform a request with the above query.
1{
2 posts {
3 id,
4 title
5 }
6}1{
2 "data": [
3 {
4 "id": 10,
5 "title": "Hello world!"
6 },
7 // ...
8 ]
9}An interesting thing above is that we don’t need to request all of the data. In our query above, we omitted the paragraphs field. Thanks to omitting fields that we don’t need, we can reduce the data transferred between our frontend and our backend.
Mutations and inputs
Aside from fetching data, we also need a way to modify it. With REST, we would send a POST request to a specific endpoint. With GraphQL, we use the same single endpoint that we’ve used for querying data.
First, we need to describe the structure of the data we use to create an entity in our database. To do that, we define an input.
1input NewPostInput {
2 title: String!
3 paragraphs: [String!]!
4}Once we’ve got the input, we can use it inside our mutation. It should return the created entity.
1type Mutation {
2 createdPost(input: NewPostInput): Post
3}Once we’ve defined both the mutation and the input, we can perform a request that creates an entity.
1mutation {
2 createPost(input: {
3 title: "Hello world!",
4 paragraphs: [
5 "Lorem ipsum",
6 "Dolor sit amet"
7 ],
8 }) {
9 id
10 title
11 paragraphs
12 }
13}Our server will respond with the created post.
1{
2 "data": {
3 "createPost": {
4 "title": "Hello world!",
5 "paragraphs": [ "Lorem ipsum", "Dolor sit amet" ],
6 }
7 }
8}Implementing GraphQL with NestJS
Now let’s implement all of the above in our application. To do that, we need to choose between two approaches.
- With the schema-first method, we describe our API in Schema Definition Language (SDL) files. NestJS analyses them and generates TypeScript definitions.
- When we implement the code-first approach, we use decorators and TypeScript classes instead of creating SDL files. NestJS generates the schema definition file out of our TypeScript code.
Since in the first paragraph we’ve written some SDL code already, we will present the code-first approach in the following example. Let’s start by installing the necessary dependencies.
1npm install @nestjs/graphql graphql-tools graphql apollo-server-expressThe first step in using GraphQL with NestJS is initializing it in our AppModule.
1import { Module } from '@nestjs/common';
2import { ConfigModule, ConfigService } from '@nestjs/config';
3import * as Joi from '@hapi/joi';
4import { GraphQLModule } from '@nestjs/graphql';
5
6@Module({
7 imports: [
8 GraphQLModule.forRootAsync({
9 imports: [ConfigModule],
10 inject: [ConfigService],
11 useFactory: (configService: ConfigService) => ({
12 playground: Boolean(configService.get('GRAPHQL_PLAYGROUND')),
13 autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
14 })
15 }),
16 ConfigModule.forRoot({
17 validationSchema: Joi.object({
18 GRAPHQL_PLAYGROUND: Joi.number(),
19 // ...
20 })
21 }),
22 // ...
23 ],
24 controllers: [],
25 providers: [],
26})
27export class AppModule {}Above, I use the GRAPHQL_PLAYGROUND environment variable to determine whether a GraphQL playground should be initialized.
1GRAPHQL_PLAYGROUND=1
2#...With the playground, we get an interactive, graphical interface that we can use to interact with our server. We can open it on the same URL as the GraphQL server.
Since we use the code-first approach, we need to define the autoSchemaFile to describe the path where our generated schema will be created.
Queries
The first thing to do when we want to query data using the code-first approach is to define a model. NestJS translates it under the hood into the object type.
1import { Field, Int, ObjectType } from '@nestjs/graphql';
2
3@ObjectType()
4export class Post {
5 @Field(() => Int)
6 id: number;
7
8 @Field()
9 title: string;
10
11 @Field(() => [String])
12 paragraphs: string[];
13}To provide instructions on how to turn GraphQL operation into data, we need to define a resolver. Thanks to using decorators, NestJS can generate SDL files from our code.
1import { Query, Resolver } from '@nestjs/graphql';
2import { Post } from './models/post.model';
3import PostsService from './posts.service';
4
5@Resolver(() => Post)
6export class PostsResolver {
7 constructor(
8 private postsService: PostsService,
9 ) {}
10
11 @Query(() => [Post])
12 async posts() {
13 const posts = await this.postsService.getAllPosts();
14 return posts.items;
15 }
16}We’ve defined the PostsService in the previous parts of this series
Thanks to creating the above resolver and adding it to our module’s providers array, we can now perform a working query.
Mutation and authentication guards
Aside from querying the data, we also want to be able to create it. To do that, we need migrations with inputs.
In the third part of this series, we’ve implemented authentication with JWT. We require the users to authenticate before creating posts. It will work just fine with GraphQL, given we adjust it a little bit.
First, we need to create a slightly modified guard.
1import { AuthGuard } from '@nestjs/passport';
2import { ExecutionContext, Injectable } from '@nestjs/common';
3import { GqlExecutionContext } from '@nestjs/graphql';
4
5@Injectable()
6export class GraphqlJwtAuthGuard extends AuthGuard('jwt') {
7 getRequest(context: ExecutionContext) {
8 const ctx = GqlExecutionContext.create(context);
9 return ctx.getContext().req;
10 }
11}We also need to create the input for our posts.
1import { InputType, Field } from '@nestjs/graphql';
2
3@InputType()
4export class CreatePostInput {
5 @Field()
6 title: string;
7
8 @Field(() => [String])
9 paragraphs: string[];
10}Once that’s all taken care of, we can add the above to our resolver.
1import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
2import { Post } from './models/post.model';
3import PostsService from './posts.service';
4import { CreatePostInput } from './inputs/post.input';
5import { UseGuards } from '@nestjs/common';
6import RequestWithUser from '../authentication/requestWithUser.interface';
7import { GraphqlJwtAuthGuard } from '../authentication/graphql-jwt-auth.guard';
8
9@Resolver(() => Post)
10export class PostsResolver {
11 constructor(
12 private postsService: PostsService,
13 ) {}
14
15 @Query(() => [Post])
16 async posts() {
17 const posts = await this.postsService.getAllPosts();
18 return posts.items;
19 }
20
21 @Mutation(() => Post)
22 @UseGuards(GraphqlJwtAuthGuard)
23 async createPost(
24 @Args('input') createPostInput: CreatePostInput,
25 @Context() context: { req: RequestWithUser },
26 ) {
27 return this.postsService.createPost(createPostInput, context.req.user);
28 }
29}Doing the above allows us to create posts and assign authors to them.
For it to work, we need to set request.credentials to same-origin in the playground settings.
We also need to open up DevTools and save the appropriate cookie.
Summary
In this article, we’ve gone through the very basics of GraphQL. We’ve introduced concepts such as queries and mutations and explained how they look in Schema Definition Language files. Aside from that, we’ve also implemented the code-first approach. By doing that, we let NestJS generate SDL for us.
There is still a lot to cover when it comes to GraphQL, so stay tuned!