Dev Tips & Tricks

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

Back to posts

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:

yarn global add @nestjs/cli
nest new server

Open the project in your preferred text editor:

code ./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.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver } from '@nestjs/apollo';
import { MovieModule } from './movie/movie.module';

@Module({
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: true, // generate schema files from typescript types on-the-fly in the memory
      //autoSchemaFile: join(process.cwd(), 'generated/schema.gql'), // The autoSchemaFile property value is the path where your automatically generated schema will be created
      driver: ApolloDriver,
      playground: true,
      validation: true,
    }),
    MovieModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export 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.
// movie/types/movie.type.ts

import { Field, ObjectType } from '@nestjs/graphql';
import { Actor } from '../actor/actor.type';

/**
 * This class is a GraphQL type that represents a Movie.
 * It defines the shape of an Movie object for all GraphQL operations.
 */

@ObjectType()
export class Movie {
  @Field(() => String)
  id: string;

  @Field(() => String)
  title: string;

  @Field(() => Number)
  year: number;

  @Field(() => String)
  director: string;

  @Field(() => [Actor])
  actors: Actor[];
}
// movie/types/actor.type.ts

/**
 * This class is a GraphQL type that represents an Actor.
 * It defines the shape of an Actor object for all GraphQL operations.
 */

import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Actor {
  @Field(() => String)
  id: string;

  @Field(() => String)
  name: string;

  @Field(() => String, { nullable: true })
  bio?: string;
}

@ObjectType() Decorator that marks a class as a GraphQL type.
@Field() decorator is used to mark a specific class property as a GraphQL field.
 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.

{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.

// movie/dto/create-movie.dto.ts

import {
  IsNumber,
  IsString,
  IsNotEmpty,
  ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { CreateActorDto } from '../actor/create-actor.dto';
import { Field, InputType } from '@nestjs/graphql';

@InputType()
export class CreateMovieDto {
  @IsNotEmpty()
  @IsString()
  @Field()
  title: string;

  @IsNotEmpty()
  @IsNumber()
  @Field()
  year: number;

  @IsNotEmpty()
  @IsString()
  @Field()
  director: string;

  @ValidateNested({ each: true })
  @Type(() => CreateActorDto)
  @Field(() => [CreateActorDto])
  actors: CreateActorDto[];
}
// movie/dto/create-actor.dto.ts

import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Actor {
  @Field(() => String)
  id: string;

  @Field(() => String)
  name: string;

  @Field(() => String, { nullable: true })
  bio?: string;
}

Step 6: Manage Data with a Service

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

import { Injectable } from '@nestjs/common';
import { CreateMovieDto } from './dto/create-movie.dto';
import { Movie } from './types/movie.type';

@Injectable()
export class MovieService {
  private movies: Movie[] = [];

  create(movieInput: CreateMovieDto): Movie {
    const actors = movieInput.actors.map((actor) => ({
      id: Date.now().toString(),
      ...actor,
    }));

    const movie = {
      id: Date.now().toString(),
      ...movieInput,
      actors,
    };
    this.movies.push(movie);
    return movie;
  }

  findAll(): Movie[] {
    return this.movies;
  }

  findOne(id: string): Movie {
    return this.movies.find((movie) => movie.id === id);
  }
}

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

// movie/movie.module.ts

import { Module } from '@nestjs/common';
import { MovieResolver } from './movie.resolver';
import { MovieService } from './movie.service';

@Module({
  providers: [MovieResolver, MovieService],
})
export 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:

mutation {
  createMovie(
    movieInput: {
      title: "Movie1"
      year: 2000
      director: "Director1"
      actors: [{ name: "Actor1", bio: "Bio1" }]
    }
  ) {
    id
    title
    year
    director
    actors {
      name
      bio
    }
  }
}

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

yarn create vite client --template react-ts
cd client
yarn

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

yarn add @apollo/client graphql

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

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";

import { ApolloProvider } from "@apollo/client";
import { ApolloClient, InMemoryCache } from "@apollo/client";

const client = new ApolloClient({
  uri: "<http://localhost:3000/graphql>",
  cache: new InMemoryCache(),
});

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </React.StrictMode>
);

Apollo VSCode Extension

For autocomplete and IntelliSense in your .graphql files:

  1. Install the Apollo GraphQL VSCode extension.
  2. Create an apollo.config.js at the root:
module.exports = {
  client: {
    service: {
      name: 'nest-server-app',
      url: '<http://localhost:3000/graphql>',
    },
  },
  includes: ['./src/**/*.graphql', './**/*.graphql'],  
};

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.

1- src/movies/graphql/getMovies.graphql

query GetMovieByID($id: String!) {
  movie(id: $id) {
    title
  }
}

query GetMovies {
  movies {
    id
    title
    actors {
      name
    }
  }
}

2- src/movies/graphql/getMovies.graphql

mutation CreateMovie($movieInput: CreateMovieDto!) {
  createMovie(movieInput: $movieInput) {
    id
    title
    director   
  }
}

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


yarn 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:

overwrite: true
schema: "<http://localhost:3000/graphql>"
documents: "src/**/*.graphql"
generates:
  generated/graphql.tsx:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
    config:
      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

import { useState, ChangeEvent, FormEvent } from "react";
import {
  useCreateMovieMutation,
  CreateMovieDto,
} from "../../generated/graphql";

const AddMovie = () => {
  const [createMovie] = useCreateMovieMutation({
    refetchQueries: ["GetMovies"], // refetch 'GetMovies' query after mutation
  });

  const [movieInput, setMovieInput] = useState<CreateMovieDto>({
    title: "",
    director: "",
    year: 0,
    actors: [],
  });

  const handleSubmit = async (event: FormEvent) => {
    event.preventDefault();
    try {
      const { data } = await createMovie({
        variables: {
          movieInput: {
            ...movieInput,
            year: +movieInput.year,
          },
        },
      });
      console.log(data);
    } catch (error) {
      console.log(error);
    }
  };

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    setMovieInput({ ...movieInput, [name]: value });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>Add New Movie</h2>
      <input name="title" placeholder="Movie Title" onChange={handleChange} />
      <input name="director" placeholder="Director" onChange={handleChange} />
      <input
        name="year"
        placeholder="Year"
        type="number"
        onChange={handleChange}
      />
      <button type="submit">Add Movie</button>
    </form>
  );
};

export 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

import "./App.css";
import AddMovie from "./movies/AddMovie";
import MoviesList from "./movies/MoviesList";

function App() {
  return (
    <>
      <h1>Movies App</h1>
      <div>
       <AddMovie />
       <MoviesList />
      </div>
    </>
  );
}

export 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!

Share with
Terms of Use | Privacy Policy