Blog
Dev Tips & Tricks

6 Easy Steps to Seamless Type Sharing with NestJS, GraphQL, and TypeScript

November 10, 2023
6 min read
Type Sharing with NestJS, GraphQL, and TypeScript - Featured Image
By
2am.

In the world of modern web development, harnessing the power of TypeScript, NestJS, and GraphQL, a powerful query language for APIs, combined with TypeScript, provides end-to-end type safety, making it an elegant alternative to traditional REST APIs.

Today, we'll guide you through an exciting journey that explores how to seamlessly integrate TypeScript with NestJS and GraphQL.

This integration not only improves the developer experience but also ensures type safety, utilizes auto-generated types, reduces the codebase size, and enhances code maintainability.

We will walk you through the process in six easy steps, making your development journey more enjoyable and efficient.

Step 1: Setup NestJS

NestJS is a powerful framework for constructing efficient, scalable Node.js server-side applications.

NestJS employs elements from various programming paradigms, including Object-Oriented Programming (OOP), Functional Programming (FP), and Functional Reactive Programming (FRP), resulting in highly maintainable and scalable code.

To begin, install the NestJS CLI globally and create a new project:

1yarn global add @nestjs/cli
2nest new server

Open the project in your preferred text editor:

1code ./server

Step 2: Incorporate GraphQL

NestJS offers the GraphQLModule, which seamlessly combines GraphQL and Apollo functionalities. Apollo stands out as one of the most dynamic and prevalent solutions the GraphQL world.

To get started, install the required packages using the command:

yarn add  graphql graphql-tools  @nestjs/graphql @apollo/server @nestjs/apollo

These packages serve the following purposes:

  • graphql: The core GraphQL package for Node.js.
  • graphql-tools: Supplies handy utilities, such as GraphQL  playground
  • @apollo/server:  Enables the use of GraphQL  in Node.js apollo-server-expressdeprecated
  • @nestjs/graphql: The official GraphQL module for NestJS's, featuring plenty of helpful decorators and tools.

Import and config GraphQLModule in app.module.ts:

The GraphQLModule essentially acts as a wrapper around the Apollo Server. it doesn't reinvent the wheel but it provides a ready-to-use module instead. This module offers a streamlined approach to working with GraphQL and NestKS in harmony.

1import { Module } from '@nestjs/common';
2import { AppController } from './app.controller';
3import { AppService } from './app.service';
4import { GraphQLModule } from '@nestjs/graphql';
5import { ApolloDriver } from '@nestjs/apollo';
6import { MovieModule } from './movie/movie.module';
7
8@Module({
9  imports: [
10    GraphQLModule.forRoot({
11      autoSchemaFile: true, // generate schema files from typescript types on-the-fly in the memory
12      //autoSchemaFile: join(process.cwd(), 'generated/schema.gql'), // The autoSchemaFile property value is the path where your automatically generated schema will be created
13      driver: ApolloDriver,
14      playground: true,
15      validation: true,
16    }),
17    MovieModule,
18  ],
19  controllers: [AppController],
20  providers: [AppService],
21})
22export class AppModule {}

Step 3: Embracing TypeScript with NestJS

Next, we'll  employ a code-first approach to define our entities: Movie and Actor . These entities will be represented as TypeScript classes, adorned with decorators to facilitate the automatic creation of our GraphQL schema.

This brings us several benefits:

  • Establishing a singular source of truth for our data structure.
  • Attainment of type safety.
  • Adopting a more intuitive way of data definition.
  • Reduced codebase size, as the GraphQL schema will be auto-generated from TypeScript classes.
1// movie/types/movie.type.ts
2
3import { Field, ObjectType } from '@nestjs/graphql';
4import { Actor } from '../actor/actor.type';
5
6/**
7 * This class is a GraphQL type that represents a Movie.
8 * It defines the shape of an Movie object for all GraphQL operations.
9 */
10
11@ObjectType()
12export class Movie {
13  @Field(() => String)
14  id: string;
15
16  @Field(() => String)
17  title: string;
18
19  @Field(() => Number)
20  year: number;
21
22  @Field(() => String)
23  director: string;
24
25  @Field(() => [Actor])
26  actors: Actor[];
27}
1// movie/types/actor.type.ts
2
3/**
4 * This class is a GraphQL type that represents an Actor.
5 * It defines the shape of an Actor object for all GraphQL operations.
6 */
7
8import { Field, ObjectType } from '@nestjs/graphql';
9
10@ObjectType()
11export class Actor {
12  @Field(() => String)
13  id: string;
14
15  @Field(() => String)
16  name: string;
17
18  @Field(() => String, { nullable: true })
19  bio?: string;
20}
1
2@ObjectType() Decorator that marks a class as a GraphQL type.
3@Field() decorator is used to mark a specific class property as a GraphQL field.
4 Only properties decorated with this decorator will be defined in the schema.

Step 4:  Resolving Data with GraphQL

Now, let's build our GraphQL resolvers to manage queries and mutations:

// movie/movie.resolver.ts

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { MovieService } from './movie.service';
import { CreateMovieDto } from './create-movie.dto';
import { Movie } from './types/movie.type';

@Resolver(() => Movie)
export class MovieResolver {
  constructor(private readonly movieService: MovieService) {}
  @Query(() => [Movie]) movies() {
    return this.movieService.findAll();
  }
  @Query(() => Movie) movie(@Args('id') id: string) {
    return this.movieService.findOne(id);
  }
  @Mutation(() => Movie) createMovie(
    @Args('movieInput') movieInput: CreateMovieDto,
  ) {
    return this.movieService.create(movieInput);
  }
}

The Graphql Schema will be automatically generated from these TypeScript types and resolvers.

Step 5: Input Validation with DTOs and Class-Validator

  • To ensure that we receive valid data from the client, we can use Data Transfer Objects (DTOs) integrated with class-validator and long list of validation decorators (see the library page on GitHub).
  • NestJS employs the concept of DTOs (Data Transfer Objects), which are classes that specify the format for data transmission across the network. When a client forwards a request to your NestJS server, the request data (e.g., JSON payload in a POST request) can be automatically mapped to an instance of a DTO class.
  • By applying class-validator decorators to the properties of this DTO class, you can enforce validation rules for incoming data. This includes the automatic generation of informative validation error messages.
  • For instance, adding the @IsNotEmpty decorator to the title field in the DTO below, results in the generation of a clear and comprehensive validation message.
1
2{errors: [{message: "Field "CreateMovieDto.title" of required type "String!" was not provided.",…}]}

Now, let’s install these packages:

yarn add class-validator class-transformer

Additional information about the top packages (for those who are curious):

Class-validator is a powerful tool that allows you to apply validation rules to JavaScript objects using decorators. It ensures that the data within an object matches certain conditions before you ise it in your application. Alongside its numerous built-in decorators for validation purposes, it offers the flexibility to create custom validation decorators. Commonly, it is used in NestJS for validating data within DTOs (Data Transfer Objects). Additionally, class-validator generates descriptive and informative validation error messages, significantly enhancing the process of debugging and error resolving.

Class-transformer is a library that facilitates transformations between classes and objects. It is primarily used to transform plain JavaScript objects into class instances, and vice versa. This library proves invaluable in managing intricate data within complex applications and can be used to selectively reveal or conceal specific fields during the transformation process.

In the context of NestJS, class-validator and class-transformer are used together for automatic validation and transformation of data through the ValidationPipe. Furthermore, in scenarios involving using GraphQL with NestJS, the @nestjs/graphql package utilizes class-validator and class-transformer to provide effective validation and transformation features.

Now let’s integrate create-movie.dto , create-actor.dto and movie.service.ts to complete our movie resolver.

1// movie/dto/create-movie.dto.ts
2
3import {
4  IsNumber,
5  IsString,
6  IsNotEmpty,
7  ValidateNested,
8} from 'class-validator';
9import { Type } from 'class-transformer';
10import { CreateActorDto } from '../actor/create-actor.dto';
11import { Field, InputType } from '@nestjs/graphql';
12
13@InputType()
14export class CreateMovieDto {
15  @IsNotEmpty()
16  @IsString()
17  @Field()
18  title: string;
19
20  @IsNotEmpty()
21  @IsNumber()
22  @Field()
23  year: number;
24
25  @IsNotEmpty()
26  @IsString()
27  @Field()
28  director: string;
29
30  @ValidateNested({ each: true })
31  @Type(() => CreateActorDto)
32  @Field(() => [CreateActorDto])
33  actors: CreateActorDto[];
34}
1// movie/dto/create-actor.dto.ts
2
3import { Field, ObjectType } from '@nestjs/graphql';
4
5@ObjectType()
6export class Actor {
7  @Field(() => String)
8  id: string;
9
10  @Field(() => String)
11  name: string;
12
13  @Field(() => String, { nullable: true })
14  bio?: string;
15}

Step 6: Manage Data with a Service

Now, let's create a simple MovieService for managing our in-memory database:

1import { Injectable } from '@nestjs/common';
2import { CreateMovieDto } from './dto/create-movie.dto';
3import { Movie } from './types/movie.type';
4
5@Injectable()
6export class MovieService {
7  private movies: Movie[] = [];
8
9  create(movieInput: CreateMovieDto): Movie {
10    const actors = movieInput.actors.map((actor) => ({
11      id: Date.now().toString(),
12      ...actor,
13    }));
14
15    const movie = {
16      id: Date.now().toString(),
17      ...movieInput,
18      actors,
19    };
20    this.movies.push(movie);
21    return movie;
22  }
23
24  findAll(): Movie[] {
25    return this.movies;
26  }
27
28  findOne(id: string): Movie {
29    return this.movies.find((movie) => movie.id === id);
30  }
31}

Next, let's add a NestJS module for movies, which has already been imported in the app.module.ts:

1// movie/movie.module.ts
2
3import { Module } from '@nestjs/common';
4import { MovieResolver } from './movie.resolver';
5import { MovieService } from './movie.service';
6
7@Module({
8  providers: [MovieResolver, MovieService],
9})
10export class MovieModule {}

Update main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    cors: {
      origin: '*', //'<http://localhost:5173>', // specify the domains that you want to allow
      methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', // specify the methods allowed
    },
  });
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

Now run  yarn start and navigate to http://localhost:3000/graphql (as defined in main.ts)

A sample mutation for adding a new movie:

1mutation {
2  createMovie(
3    movieInput: {
4      title: "Movie1"
5      year: 2000
6      director: "Director1"
7      actors: [{ name: "Actor1", bio: "Bio1" }]
8    }
9  ) {
10    id
11    title
12    year
13    director
14    actors {
15      name
16      bio
17    }
18  }
19}

Now Let's add the client-side to use our movie API using React, Apollo Client, and the graphql-code-generator package.

1yarn create vite client --template react-ts
2cd client
3yarn

Next, let’s install the necessary packages for GraphQL client:

yarn add @apollo/client graphql

In src/main.tsx, set up the Apollo Client:

1import React from "react";
2import ReactDOM from "react-dom/client";
3import App from "./App.tsx";
4import "./index.css";
5
6import { ApolloProvider } from "@apollo/client";
7import { ApolloClient, InMemoryCache } from "@apollo/client";
8
9const client = new ApolloClient({
10  uri: "<http://localhost:3000/graphql>",
11  cache: new InMemoryCache(),
12});
13
14ReactDOM.createRoot(document.getElementById("root")!).render(
15  <React.StrictMode>
16    <ApolloProvider client={client}>
17      <App />
18    </ApolloProvider>
19  </React.StrictMode>
20);

Apollo VSCode Extension

For autocomplete and IntelliSense in your .graphql files:

Install the Apollo GraphQL VSCode extension.
Create an apollo.config.js at the root:

1module.exports = {
2  client: {
3    service: {
4      name: 'nest-server-app',
5      url: '<http://localhost:3000/graphql>',
6    },
7  },
8  includes: ['./src/**/*.graphql', './**/*.graphql'],  
9};

Make sure the movie API app is running (in server folder run yarn start)

if IntelliSense and autocomplete features are not working:

1) Open the command palette with Ctrl + Shift + P, type Reload Window:

2) Then click on Apollo extension icon:

Let add us now add some queries and mutation to test the autocomplete and IntelliSense support.

Test the autocomplete and IntelliSense support

1- src/movies/graphql/getMovies.graphql

1query GetMovieByID($id: String!) {
2  movie(id: $id) {
3    title
4  }
5}
6
7query GetMovies {
8  movies {
9    id
10    title
11    actors {
12      name
13    }
14  }
15}

2- src/movies/graphql/getMovies.graphql

1mutation CreateMovie($movieInput: CreateMovieDto!) {
2  createMovie(movieInput: $movieInput) {
3    id
4    title
5    director   
6  }
7}

Add GraphQL Code Generator for generating TypeScript types and React hooks from GraphQL queries and mutations:

1
2yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

Add package.json scripts section:

"gen-types": "npx graphql-codegen",

Add codegen.yml in root folder:

1overwrite: true
2schema: "<http://localhost:3000/graphql>"
3documents: "src/**/*.graphql"
4generates:
5  generated/graphql.tsx:
6    plugins:
7      - "typescript"
8      - "typescript-operations"
9      - "typescript-react-apollo"
10    config:
11      withHooks: true

Run yarn gen-types to generate the required TypeScript types and React hooks based on your .graphql files. This will produce hooks like (useGetMoviesQuery and useCreateMovieMutation).

Next, let’s add movies components (that use those generated types and hooks):

src/movies/AddMovie.tsx

1import { useState, ChangeEvent, FormEvent } from "react";
2import {
3  useCreateMovieMutation,
4  CreateMovieDto,
5} from "../../generated/graphql";
6
7const AddMovie = () => {
8  const [createMovie] = useCreateMovieMutation({
9    refetchQueries: ["GetMovies"], // refetch 'GetMovies' query after mutation
10  });
11
12  const [movieInput, setMovieInput] = useState<CreateMovieDto>({
13    title: "",
14    director: "",
15    year: 0,
16    actors: [],
17  });
18
19  const handleSubmit = async (event: FormEvent) => {
20    event.preventDefault();
21    try {
22      const { data } = await createMovie({
23        variables: {
24          movieInput: {
25            ...movieInput,
26            year: +movieInput.year,
27          },
28        },
29      });
30      console.log(data);
31    } catch (error) {
32      console.log(error);
33    }
34  };
35
36  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
37    const { name, value } = event.target;
38    setMovieInput({ ...movieInput, [name]: value });
39  };
40
41  return (
42    <form onSubmit={handleSubmit}>
43      <h2>Add New Movie</h2>
44      <input name="title" placeholder="Movie Title" onChange={handleChange} />
45      <input name="director" placeholder="Director" onChange={handleChange} />
46      <input
47        name="year"
48        placeholder="Year"
49        type="number"
50        onChange={handleChange}
51      />
52      <button type="submit">Add Movie</button>
53    </form>
54  );
55};
56
57export default AddMovie;

src/movies/MoviesList.tsx

import { useGetMoviesQuery } from "../../generated/graphql";

const MoviesList = () => {
  const { data, loading, error } = useGetMoviesQuery();

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return (
    <div>
      <h2>Movies List</h2>
      {data?.movies.map((movie) => (
        <div key={movie.id}>
          <h3>{movie.title}</h3>
          {/* <p>Directed by: {movie.}</p> */}
        </div>
      ))}
    </div>
  );
};

export default MoviesList;

and update the App component:

1import "./App.css";
2import AddMovie from "./movies/AddMovie";
3import MoviesList from "./movies/MoviesList";
4
5function App() {
6  return (
7    <>
8      <h1>Movies App</h1>
9      <div>
10       <AddMovie />
11       <MoviesList />
12      </div>
13    </>
14  );
15}
16
17export default App;

And now, run  yarn dev  in client folder to see the app!

With this, our application is equipped to handle both creating new movies and querying existing ones, complete with validation!

By embracing the combined power of NestJS, GraphQL, and TypeScript, we've successfully created a robust, efficient, and type-safe app.

This seamless integration not only improves our development process but also guarantees the reliability and scalability of our application.

Feel free to try it out and enjoy the coding experience!

Accelerate Your Career with 2am.tech

Join our team and collaborate with top tech professionals on cutting-edge projects, shaping the future of software development with your creativity and expertise.

See Open Positions

Don't miss out on
our latest insights
– Subscribe Now!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Share This Post
Back to Blog
Don't miss out on
our latest insights
– Subscribe Now!
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Navigate
Start Now