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.jsapollo-server-express
deprecated@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.
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