Jak użyć technologii GraphQL w frameworku NestJS?

Jak użyć technologii GraphQL w frameworku NestJS?

Czym jest GraphQL i NestJS warto żeby wiedział każdy programista. Co powstanie z ich połączenia? Jak stworzyć aplikację serwerową przy użyciu GraphQL w frameworku NestJS?

GraphQL to standard tworzenia API zaprojektowany i stworzony przez programistów Facebook’a. Jest to sposób komunikacji z serwerem jako alternatywa REST’a. Dostarcza on jeden endpoint, który służy do zwracania lub modyfikowania danych.

NestJS to framework napisany w Node.js w celu szybkiego oraz wydajnego tworzenia aplikacji serwerowych. Stworzony został w 2018 roku przez naszego rodaka Kamila Myśliwca. Nest.js posiada zalety Javascript z pełnym wsparciem Typescript oraz dodatkowo łączy OOP – programowanie zorientowane obiektowo wraz z FP – programowaniem funkcyjnym.

W poniższym przykładzie użyjemy również bazy MySQL z użyciem TypeORM, aby zapisać wprowadzane dane.

W celu zapoznania się z działaniem tych elementów stworzymy moduł CRUD dla użytkowników, gdzie jeden użytkownik będzie miał imię, nazwisko oraz email. Dodatkowo dodamy odpowiednią walidację danych.

Gotowy? W takim razie zaczynajmy…

Od czego zacząć?

W celu rozpoczęcia potrzebne będzie oczywiście środowisko Node.js. Jeśli jeszcze go nie zainstalowałeś, to odsyłam Cię na oficjalną stronę w celu instalacji.

Rozpocząć projekt można na dwa sposoby. Pierwszym z nich jest instalacja Nest CLI, a następnie stworzenie nowego projektu poprzez wywołanie odpowiedniej komendy.

Drugi to sklonowanie plików z oficjalnego repozytorium i zainstalowanie bibliotek node. Obydwa sposoby doprowadzą do tego samego rezultatu. Ja skorzystam z pierwszej opcji.

Stworzenie projektu

Instalacja Nest CLI następuje poprzez wpisanie poniższej komendy:

npm i -g @nestjs/cli

Po zainstalowaniu zyskujemy dostęp do komendy nest. Jeśli po jej wpisaniu otrzymujesz poniższy wynik, to znaczy, że proces instalacji przebiegł pomyślnie.

komenda nest

Stworzenie projektu to tak naprawdę wpisanie poniższej komendy.

nest new graphql-article

Stworzenie projektu przy użyciu komendy nest

Podczas tworzenia należy wybrać jakiego systemu do paczkowania chcemy używać – npm lub yarn. Ja użyłem npm’a. Jak już pewnie zauważyłeś „graphql-article” to tak naprawdę nazwa katalogu pod którym stworzył się projekt. Dostęp do wszystkich komend znajdziesz na oficjalnej stronie autora.

Następnie w celu przetestowania czy wszystko przebiegło pomyślnie, należy przejść do katalogu ze stworzonym projektem oraz wpisać komendę:

npm start

Uruchomienie projektu przy użyciu komendy npm

Przechodząc pod adres http://localhost:3000 zauważymy, jakże sławne Witaj Świecie!

hello-world

Instalacja bibliotek

W celu stworzenia aplikacji z założonych wyżej wymagań potrzebne będą biblioteki, które nie są dołączone w bazowym projekcie. Należy uruchomić poniższą komendę w celu instalacji.

npm i –save @nestjs/graphql [email protected] [email protected]@[email protected]/typeorm typeorm mysqlclass-validator

Następnie należy skonfigurować następujące biblioteki. Uruchomiamy projekt w wybranym edytorze. Ja osobiście używam PHPStorm’a.

//app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import {GraphQLModule} from '@nestjs/graphql';
import {TypeOrmModule} from '@nestjs/typeorm';

@Module({
imports: [
    TypeOrmModule.forRoot({
type       : 'mysql',
host       : 'localhost',
port       : 3306,
username   : 'username',
password   : 'password',
database   : 'graphql-article',
entities   : [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
charset    : 'utf8mb4_unicode_ci',
logging    : true
}),
GraphQLModule.forRoot({
debug         : true,
playground    : true,
autoSchemaFile: 'schema.gql'
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

Wskazówka: Dobrą praktyką jest dodawanie wszystkich wrażliwych danych do zmiennych środowiskowych.

Należy również stworzyć bazę danych o nazwie „graphql-article”. Oczywiście dane do połączenia z bazą danych należy podać własne.

Stworzenie modułu użytkowników

W module użytkowników stworzymy serwis, resolver, repozytorium, encję oraz użytkownika graphql, który będzie odpowiadał za model danych. Dodatkowo stworzymy dwie klasy: schemat odpowiadający za pojedynczego użytkownika oraz schemat odpowiadający za manipulację rekordami listy. Obie te klasy odnoszą się do DTO – Data Transfer Object.

Tworzenie modułów w Nest.js można przeprowadzić poprzez ręczne stworzenie plików bądź poprzez wywołane komendy. Ja polecam ten drugi sposób. A więc zaczynajmy.

Moduł

nest generate module users

Struktura modułu użytkowników

Encja

//users/user.entity.rs

import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm';

@Entity()
export class UserEntity {

@PrimaryGeneratedColumn()
id: number;

@Column({nullable: true})
firstName: string | null;

@Column({nullable: true})
lastName: string | null;

@Column({nullable: true, unique: true, length: 191})
email: string;
}

Repozytorium

//users.repository.ts

import {EntityRepository, Repository} from 'typeorm';
import {UserEntity} from './user.entity';

@EntityRepository(UserEntity)
export class UsersRepository extends Repository<UserEntity> {

}

Użytkownik graphql

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

@ObjectType()
export class UserGraphql {

@Field(type =>Number)
id: number;

@Field({nullable: true})
firstName: string | null;

@Field({ nullable: true })
lastName: string | null;

@Field()
email: string;
}

Schemat odpowiadający za pojedynczego użytkownika

//users/dto/user.input.ts
import {Field, InputType} from '@nestjs/graphql';
import {IsEmail, IsOptional, IsString} from 'class-validator';

@InputType()
export class UserInput {

@Field({ nullable: true })
@IsString()
@IsOptional()
firstName?: string;

@Field({ nullable: true })
@IsOptional()
@IsString()
lastName?: string;

@Field()
@IsEmail()
email: string;
}

Schemat odpowiadający za manipulację rekordami listy

//users/dto/users.args.ts
import {ArgsType, Field, Int} from '@nestjs/graphql';
import {Max, Min} from 'class-validator';

@ArgsType()
export class UsersArgs {

@Field(type =>Int, {defaultValue: 0, description: "Define how many records to skip"})
@Min(0)
skip: number = 0;

@Field(type =>Int, {defaultValue: 25, description: "Define how many record to take"})
@Min(1)
@Max(50)
take: number = 25;
}

Serwis

nest generate service users

Po wygenerowaniu serwisu implementujemy poniższe metody

//users/users.service.ts
import { Injectable } from '@nestjs/common';
import {UserEntity} from './user.entity';
import {UserInput} from './dto/user.input';
import {UsersRepository} from './users.repository';

@Injectable()
export class UsersService {
constructor(
private readonly usersRepository: UsersRepository,
) {
    }

async create(userInput: UserInput): Promise<UserEntity>{
const anotherUserWithGivenMail = await this.usersRepository.findOne({email: userInput.email});
    if(anotherUserWithGivenMail) {
throw new BadRequestException('Another user with given email already exists');
}
const user = new UserEntity();
    this.fillUser(userInput, user);
    return this.usersRepository.save(user);
}

async update(id: number, userInput: UserInput): Promise<UserEntity>{
const anotherUserWithGivenMail = await this.usersRepository.findOne({
where: {
id: Not(Equal(id)),
email: Equal(userInput.email)
        }
    });
    if(anotherUserWithGivenMail) {
throw new BadRequestException('Another user with given email already exists');
}
const user = await this.usersRepository.findOneOrFail({id});
    this.fillUser(userInput, user);
    return this.usersRepository.save(user);
}

fillUser(userInput: UserInput, user: UserEntity) : void{
    user.firstName = userInput.firstName;
user.lastName = userInput.lastName;
user.email = userInput.email;
}

}

Resolver

nest generate resolver users

Po wygenerowaniu resolver’a następuje implementacja poniższych metod

//users/users.resolver.ts
import {Args, Mutation, Query, Resolver} from '@nestjs/graphql';
import {UsersArgs} from './dto/users.args';
import {UserEntity} from './user.entity';
import {UserGraphql} from './user.graphql';
import {UsersService} from './users.service';
import {UserInput} from './dto/user.input';
import {UsersRepository} from './users.repository';

@Resolver('Users')
export class UsersResolver {

constructor(
private readonly usersService: UsersService,
        private readonly usersRepository: UsersRepository
    ) {
    }

@Query(returns => [UserGraphql], {
description: "Get all users"
})
async getUsers(
@Args() usersArgs: UsersArgs
    ): Promise<UserEntity[]> {
return this.usersRepository.find({
take: usersArgs.take,
skip: usersArgs.skip,
});
}

@Query(returns => UserGraphql)
async getUser(
@Args('id') id: number,
): Promise<UserEntity> {
return this.usersRepository.findOne({id});
}

@Mutation(returns => UserGraphql)
async createUser(
@Args('user') userInput: UserInput,
): Promise<UserEntity> {
return await this.usersService.create(userInput);
}

@Mutation(returns => UserGraphql)
async updateUser(
@Args('id') id: number,
@Args('user') userInput: UserInput
    ): Promise<UserEntity> {
return await this.usersService.update(id, userInput);
}

@Mutation(returns =>Boolean)
async removeUser(@Args('id') id: number) {
await this.usersRepository.delete(id);
        return true;
}


}

Ostatecznie należy jeszcze skonfigurować plik modułu.

//users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersResolver } from './users.resolver';
import {TypeOrmModule} from '@nestjs/typeorm';
import {UsersRepository} from './users.repository';

@Module({
imports: [
    TypeOrmModule.forFeature([UsersRepository])
  ],
providers: [UsersService, UsersResolver]
})
export class UsersModule {}

Finalnie struktura modułu użytkowników wygląda następująco:

Finalna struktura modułu użytkowników

W celu użycia stworzonego modułu należy go załączyć w pliku „app.module.ts”, który ostatecznie wygląda tak:

//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import {GraphQLModule} from '@nestjs/graphql';
import {TypeOrmModule} from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';

@Module({
imports: [
    TypeOrmModule.forRoot({
type       : 'mysql',
host       : 'localhost',
port       : 3306,
username   : 'username',
password   : 'password',
database   : 'graphql-article',
entities   : [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
charset    : 'utf8mb4_unicode_ci',
logging    : true
}),
GraphQLModule.forRoot({
debug         : true,
playground    : true,
autoSchemaFile: 'schema.gql'
}),
UsersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

Gratulacje, przeszedłeś przez cały proces konfiguracji oraz implementacji wymaganych metod.

Testy

Wskazówka: Jeśli jeszcze tego nie zrobiłeś, to stwórz bazę danych o nazwie “graphql-article”.

Uruchamiamy projekt komendą:

npm start

Po uruchomieniu zostaje stworzona tabela w bazie danych na podstawie skonfigurowanej encji użytkownika „user.entity”. Ja w celu zobrazowania sytuacji użyję klienta bazy danych o nazwie MySQL Workbench.

Środowisko do testowania znajduje się pod adresem http://localhost:3000/graphql

środowisko-do-testowania

A więc do dzieła stwórzmy użytkownika oraz zwróćmy jego dane!

tworzenie-użytkownika

Jak widać w bazie danych pojawił się nowy wpis.

Widok stworzonego użytkownika w bazie danych

Następnie przeprowadźmy edycję stworzonego wcześniej użytkownika.

edycja użytkownika
Widok edytowanego użytkownika w bazie danych

Jak zauważyłeś proces edycji przebiegł pomyślnie.

Usunięcie użytkownika

usunięcie-użytkownika
Widok bazy danych po usunięciu użytkownika

Jak widzisz wpis o użytkowniku został usunięty z bazy danych.

Natomiast co z pobieraniem użytkowników?

W tym celu stworzyłem kilka rekordów w bazie danych.

Widok testowych użytkowników w bazie danych

Pobranie pojedynczego użytkownika

Pobranie pojedynczego użytkownika

Pobranie listy użytkowników.

Pobranie listy użytkowników

Podsumowując użycie GraphQL w Nest.js to uważam, że narzędzia jakie są dostarczane przez te dwie technologie pozwalają na szybkie i wygodne budowanie aplikacji serwerowej. Sam GraphQL daję więcej możliwości dla frontend’owców, dzięki łatwiejszej manipulacji zwracanych danych oraz jednolitej generowanej dokumentacji. Natomiast połączenie z Nest.js pozwala na czytelną oraz przyjazną strukturę aplikacji.

A Ty co sądzisz o połączeniu GraphQL z Nest.js? Czy Twoim zdaniem jest to przyszłość budowania aplikacji serwerowych? Chętnie z Tobą podyskutuję na ten temat.

Resources

Link do repozytorium

Spodobał Ci się artykuł?

Masz pytania? Zacznijmy rozmowę! 🙂

Klikając wyślij, zgadzasz się na wykorzystanie Twojego adresu email oraz imienia do kontaktu elektronicznego, a także do przesyłania wiadomości marketingowych. Administratorem Twoich danych osobowych jest Graphicbox Sp. z o. o. ul. Lwowska 6/201, Rzeszów. Więcej w polityce prywatności.

Form Men