GraphQL API для Angular с помощью NX и Nest.

Aleksandr Serenko
F.A.F.N.U.R
Published in
9 min readDec 23, 2019

В данной статье поговорим, как развернуть девсервер с помощью Nx, работающий на NestJs с настроенным Grapql сервером (Apollo). Для работы с базой данных будем использовать TypeORM. А также добавим базовую функциональность авторизации и несколько базовых сущностей для демонстрации работы API.

Создание каркаса приложения

Так как Nx поддерживает из коробки создание backend приложения с помощью express или nest, создадим новое приложение

ng g @nrwl/nest:app backend/api --frontendProject=frontend-markup

Так как статья является продолжением цикла статей про Angular, приложение будем создавать в уже в существующем workspace nx’а. Все ниже перечисленное также будет работать и на только что созданном окружении Nx’а. или просто выполните несколько команд:

yarn create nx-workspace myworkspace
cd myworkspace
ng add @nrwl/nest
# Теперь можно запустить генерацию приложения
ng g @nrwl/nest:application backend/api

Запустим сгенерированное приложение:

ng serve backend-api

Если вы использовали чистую установку, то может понадобиться установка дополнительных зависимостей. Чаще всего это несколько компонентов webpack’а.

yarn add -D webpack webpack-cli webpack-sources webpack-merge

Если всё прошло успешно, то мы можем обратиться по адресу:

http://localhost:3333/api

В ответ получим сообщение:

{
"message": "Welcome to backend/api!"
}

Подготовка базы данных

Настроим базу данных для нашего проекта. Для примера мы будем использовать PostqreSQL.

Если у вас нет предустановленного сервер баз данных, то лучшим решением будет создать его с помощью docker.

Если у вас не настроен docker и docker-compose, то сначала нужно установить Docker. Инструкция по установке и настройке докера можно посмотреть здесь: Ubuntu, Fedora, Windows или MacOS.

Создадим директорию docker-compose. Внутри директории создадим файл конфигурации docker-compose.yml:

version: '3.1'services:
postgres:
image: postgres:latest
restart: on-failure
environment:
- POSTGRES_DB=medium
- POSTGRES_PASSWORD=123456
ports:
- 5432:5432
networks:
- backend
networks:
backend:

Запустим наши контейнеры:

docker-compose up -d

Docker скачает и настроит последние образы для postgres. Как видно из настроек, мы создаём сервер с базой данной “medium”, и пользователя postgres с паролем — “123456”.

После установки, проверим запущенные контейнеры:

docker ls

База данных создана, и можно заняться настройкой TypeORM.

Подключение TypeORM

Добавим необходимые зависимости:

yarn add @nestjs/typeorm typeorm pg

Заметим, что для каждой базы данных требуется свой NodeJS адаптер. Так как мы используем PostgreSQL, то устанавливаем pg, если бы использовалась бы MySQL, тогда нужно было бы установить пакет mysql.

Теперь нужно прописать параметры подключения к базе данных. Создадим в корне проекта файл ormconfig.json:

{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "postgres",
"password": "123456",
"database": "medium",
"synchronize": true,
"entities": ["dist/apps/backend/api/src/**/*.entity.js"],
"migrations": ["dist/apps/backend/api/migrations/*.js"]
}

Есть один очень важный нюанс работы webpack с TypeORM. Из-за особенности реализации, webpack не может корректно загружать и обрабатывать entities и migrations написанные на Typescript. Поэтому, мы явно в конфиге указываем, что приложение смотрит на папку — dist и ищет файлы по определённому шаблону.

На время удалим сгенерированный файл сервиса — app.service.ts, перенеся логику сразу в контроллер — app.controller.ts. Обновим app.module, поправив импорты.

Дальше в статье, мы вообще откажемся от использования контроллеров, так как все будет работать через grapql server. Поэтому эти настройки нужны только для упрощения тестирования работоспособности конфигурации.

Добавим в app.module.ts импорт библиотеки TypeORM:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { AppController } from './app.controller';

@Module({
imports: [
TypeOrmModule.forRoot()
],
controllers: [AppController],
providers: [],
})
export class AppModule {}

Перезапустим сервер, удостоверившись, что все работает.

Подключение GraphQL

Добавим необходимые зависимости для graphql:

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

Объявим тестовый запрос к Grapql серверу. Для этого создадим файл app.graphql:

type Query {
test: String
}

Запрос без параметров, возвращает строку. Опишем возвращаемые данные. Создадим тестовый app.resolver.ts:

import { Args, Resolver, Query } from '@nestjs/graphql';@Resolver('App')
export class AppResolver {
@Query('test')
async getLogin(): Promise<string> {
return `It's graphql response`;
}
}

Подключим graphql модуль и app.resolver в app.module.ts:

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';

import { AppController } from './app.controller';
import { AppResolver } from './app.resolver';

@Module({
imports: [
TypeOrmModule.forRoot(),
GraphQLModule.forRoot({
typePaths: ['./**/*.graphql'],
context: ({ req }) => ({ req }),
playground: true
}),
],
controllers: [AppController],
providers: [AppResolver],
})
export class AppModule {}

Перезапустим сервер. После перезапуска, обратимся к встроенному graphql playground’у перейдя по ссылке:

http://localhost:3333/graphql

Создадим наш запрос, введя в левую часть:

{
test
}

Нажмём кнопку play:

В результате выполнения запроса, мы получили данные:

{
"data": {
"test": "It's graphql response"
}
}

Отлично, сервер работает. Теперь можно заняться написанием базовых сущностей.

Создание моделей

Опишем интерфейс для пользователя:

Добавим интерфейс для медиа ресурсов (изображения, видео, и т.д.):

Добавим новый тип — Locale, для сохранения переводов в БД:

И создадим интерфейс для вывода событий формата сайта Lamborghini

Добавим интерфейс для авторизации:

Все модели сохраним в библиотеке моно репозитория, которую назовём entities:

ng g lib entities

Создание модуля пользователя

Создадим новый модуль users.

В папке apps/backend/api/src/app создадим папку users.

Добавим папку entities. В папке entities создадим файл user.entity.ts:

Так как мы используем workspace — medium-stories, то все базовые модели хранятся в библиотеке монорепо entities. Если вы используете свой workspace, то вам необходимо изменить пути до моделей Media, User и остальных.

Основы работы с TypeORM можно посмотреть по ссылке.

Создадим user.service.ts, который будет отвечать за выборку данных из БД:

Добавим декоратор для трансформации данных, который будет трансформировать данные в User. Это будет использоваться в дальнейшем, когда будет реализована авторизация.

Создадим GqlAuthGuard, который будет отвечать за политику безопасности и контроля доступа к определенным запросам, страницам:

Добавим user.resolver.ts, который будет связывать graphql запросы с запросами к БД:

Создадим запросы к graphql серверу — users.graphql:

Для поддержки scalar типов graphql вынесем их в app.graphql

Установим некоторые зависимости:

yarn add graphql-type-json

Для корректной трансформации scalar типов создадим resolverMap:

Создадим users.module.ts и подключим все ранее созданные классы и resolver’ы:

Подключим UserModule в app.module.ts:

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';

import { UsersModule } from './users/users.module';

import { AppController } from './app.controller';
import { resolverMap } from './app.resolver';

@Module({
imports: [
TypeOrmModule.forRoot(),
GraphQLModule.forRoot({
typePaths: ['./**/*.graphql'],
context: ({ req }) => ({ req }),
playground: true,
resolvers: [resolverMap]
}),
UsersModule,
],
controllers: [AppController]
})
export class AppModule {}

Создание media, events

Проделаем аналогичные шаги для media и добавим класс сущности, сервис и resolver, а также подключим это все в модуле — medias.module.ts.

Создадим папку medias. Создадим все требуемые классы:

Создадим папку events. Создадим все требуемые классы для event:

Единственное отличие, что в EventService есть настройка для загрузки sub entities.

Это фактически, ограничение вложенности обработки сущностей EntityManager’ом. Основная функциональность есть, но мы добавим ещё авторизацию.

Авторизация с помощью GrapgQL

Реализовывать авторизацию будем предложенным Nest.js способом — подробнее здесь.

В качестве токена авторизации будем использовать JWT.

Добавим необходимые зависимости:

yarn add @nestjs/passport passport @nestjs/jwt passport-jwt bcrypt
yarn add -D @types/passport-jwt

Создадим модуль — auth.

Вы можете использовать nest/cli, но для этого, пакет должен быть установлен глобально. Например: nest g module auth

Так как хранить пароль в открытом виде грех, то создадим PassportService для получения хеша:

Для того, чтобы получать текущего пользователя из Request’а, добавим JwtStrategy:

Создадим AuthService, который будет для заданной пары логина и пароля, пытаться авторизовать пользователя, в противном случае, отдавать 401 ошибку:

Добавим AuthResolver, который свяжет graphql запросы с бд:

Декоратор — SignIn, всего лишь трансформирует данные в пару username, password:

Опишем сами запросы graphql:

И подключим все в auth.module:

Соберём все в месте в app.module.ts:

Компиляция сущностей и миграций

Протестируем, все написанное, но для этого, нужно немного костылей :(

Для корректной работы TypeORM и нашей лени (и проблем webpack’а), постоянно указывать относительные пути к сущностям, мы скомпилируем все сущности *.entity.ts в *.entity.js

После этого, nest.js без конфликтов сможет запустить TypeORM и модифицировать базу данных.

Создадим в приложении 2 файла (tsconfig.entities.json, tsconfig.migrations.json):

Первый файл описывает параметры для компиляции сущностей, второй для миграций.

Для компиляции сущностей будем использовать команду:

node_modules/.bin/tsc --project apps/backend/api/tsconfig.entities.json

Так как у нас уже описаны все сущности, ее можно запустить сразу.

Создадим несколько миграций:

node_modules/.bin/ts-node node_modules/.bin/typeorm migration:create -n Users
node_modules/.bin/ts-node node_modules/.bin/typeorm migration:create -n Medias
node_modules/.bin/ts-node node_modules/.bin/typeorm migration:create -n Events

И заполним их следующим содержимым:

Для компиляции и запуска миграций выполним команду:

node_modules/.bin/tsc --project apps/backend/api/tsconfig.migrations.json && node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:run

Так, данные созданы, но для Media нам нужны картинки. Создадим папку uploads в папке assets. И добавим несколько изображений.

Так как в дев версии сервера, нет картинок, нужно запустить команду билда сервера (или просто скопировать изображения в папку dist):

ng build backend-api

Раздача статики

Так как у нас есть изображения, нужно разрешить доступ к ним. Есть несколько способов, но пойдём самым простым с использованием fastify.

Установим необходимые зависимости:

yarn add @nestjs/platform-fastify apollo-server-fastify fastify-static

Изменим main.ts:

Как можно заметить, пути к ресурсам прописаны явно:

app.useStaticAssets({
root: join(__dirname, './assets/uploads'),
prefix: '/uploads/'
});

Перезапустим сервер.

Запуск API сервера

Так как все установленно, собрано и настроено, протестируем наш API сервер.

Запустим graphql playground:

http://localhost:3333/graphql

Начнём с простых запросов. Запросим список эвентов:

{
events {
id
title
created
}
}

Запрос с выбором отношений:

{
events {
id
title
created
image {
id,
src,
published
}
}
}

Публичные методы работают отлично. Теперь протестируем запросы на закрытые части. Например, получение информации о текущем пользователе:

{
user {
username
created
}
}

Запрос авторизации с неверными логином и паролем:

{
login(username: "admin", password: "123") {
accessToken
expiresIn
id
}
}

Запрос с верными логином и паролем:

{
login(username: "admin", password: "123456") {
accessToken
expiresIn
id
}
}

И запрос пользователя, но уже с указанными в заголовке acceccToken’ом:

{
user {
username
created
}
}
# Http headers (on bottom section)
{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwidXNlcklkIjoxLCJpYXQiOjE1NzcxMTYzMzMsImV4cCI6MTU5MjExNjMzM30.9ToneBu0tCkiDy6_ppub8zrjoxBP6doprZ1zjoK4Mg4"
}

Все отлично работает.

Запросим файл с сервера, которой нет :

http://localhost:3333/uploads/image-1.jpg

Запросим файл, который есть на сервере:

http://localhost:3333/uploads/image-1.webp

Так как, есть шанс что это Photoshop, вот небольшая гифка для успокоения нервов:

Заключение

В ходе данной статьи было сделано:

  • Настроен Nest.js сервер.
  • Для управления БД была создана конфигурация для docker-compose
  • Для сервера была настроена ORM — TypeORM
  • Для реализации GraphQL был настроен apollo-server
  • Были реализованы базовые сущности для демонстрации работы API
  • Была настроена авторизация с помощью GraphQL
  • Была добавлена функциональность раздачи статики
  • Были сделаны скринкасты нескольких запросов.

В следующей статье интегрируем API в Angular.

Спасибо за внимание!

Исходники

Все исходники находятся на github, в репозитории — https://github.com/Fafnur/medium-stories

Демо можно посмотреть в проекте backend/api.

Для того, чтобы увидеть проект на момент написания статьи, нужно выбрать tag — backend-api.

git checkout backend-api

Предыдущие статьи:

  1. Статья про LocalStorage, SessionStorage в Angular Universal
  2. Статья про мультиязычность в Angular & Universal с помощью ngx-translate
  3. Статья про Redux в Angular с помощью Ngrx. Создание Store в Angular
  4. Статья про Настройка CSS предпроцессора в Angular с Nx
  5. Интеграция CSS framework’ов в Angular или темезируем Angular с помощью Material и Bootstrap
  6. Статья про Динамическое управление адаптивностью с помощью Angular и Redux

--

--

Aleksandr Serenko
F.A.F.N.U.R

Senior Front-end Developer, Angular evangelist, Nx apologist, NodeJS warlock