目次
本記事について
普段インターン先で使用しているbackendの主な技術スタックの概念を、復習を兼ねてまとめたものです。
NestJSとは?
TypeScriptでバックエンドAPIを作るためのフレームワーク
-
Reactがフロントエンドを作るためのフレームワーク/ライブラリだとすると、NestJSはサーバー側を作るためのもの - JavaScript/TypeScriptをサーバー側で動かすための実行環境である
Node.jsで、バックエンドを作りやすくするためのフレームワークがNestJS -
Expressも同様に有名だが、ファイルをどう分けるかなどがバラバラになりやすい-
NestJSはMVC(Model-View-Controllerの構成)で整理することができる
-
NestJSの基本構造
-
Controller=HTTPリクエストを受け取る場所
以下のように書くと、ブラウザで例えばhttp://localhost:3000/helloにアクセスすると、appService.getHello()が実行されることになる
@Controller('hello')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
-
Service=実際の処理を書く場所
ビジネスロジックを書く。Controllerに全部書くのではなく、Serviceにメインの処理を書き分けることでディレクトリの構造がわかりやすくなる
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
-
Module=ControllerやServiceをまとめて、NestJSにモジュールとして登録する場所
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
NestJSの導入方法
npx @nestjs/cli new . --skip-git --package-manager npm
をbackendディレクトリで実行してNestJSの雛形を作る。(ルートで既にgit管理している場合は、--skip-gitでbackend用に別のgitを作らせない)
このコマンドによりbackend/src/にできる主なファイル
| ファイル | 役割 |
|---|---|
| main.ts | アプリの入口。サーバーを起動する(デフォルト3000番) |
| app.module.ts | モジュール。機能をまとめる箱。NestJSの中心概念 |
| app.controller.ts | コントローラ。HTTPリクエストの受け口(今はREST) |
| app.service.ts | サービス。実際のロジックを書く場所 |
上述したようにNestJSは「Controller(受け口)→ Service(処理)」と役割を分ける設計が基本。
デフォルトではHTTPリクエスト(POST, GETなど)のエントリーポイントがapp.controller.tsでREST APIになっている。
もし、GraphQL APIを使用する場合は、以下の表から分かるように、RESTのControllerをGraphQLのResolverに置き換えていくということ。
| REST(Controller)API | GraphQL(Resolver)API | |
|---|---|---|
| クラスの目印 | @Controller() | @Resolver() |
| 取得(読み取り) | @Get() | @Query() |
| 作成・更新など | @Post() @Put() @Patch() @Delete() | @Mutation() |
| 入口URL | http://localhost:3000/ | http://localhost:3000/graphql |
要は簡単に言えばどちらも、HTTPリクエストを受け取って、HTTPレスポンスを返すAPIです。
GraphQLの場合、画面ごとに必要なデータが異なる場合には、取得するデータを分けることも可能のため、柔軟性が高いです。
REST APIとGraphQL APIについて
どちらもフロントエンドとバックエンドがデータをやり取りするためのAPIの方式
React / Flutter などのフロントエンド
↓
REST または GraphQL
↓
NestJS などのバックエンド
↓
Prisma
↓
PostgreSQL
RESTとは?
URLとHTTPメソッドで操作を表すAPI方式
GET /users/1
というリクエストをサーバーに送ったら、バックエンドは例えば以下のようなJSONを返す。
{
"id": 1,
"name": "James",
"email": "james@example.com",
"age": 10
}
返すデータの形は基本的にサーバー側が決定する。
ただ、フロントエンド側が本当はnameだけ欲しくても、APIがemailやageも返す設計なら、不要なそれらも取得してしまうというデメリットもある。
RESTのメリット
シンプルで分かりやすいこと
GET /users
POST /users
PATCH /users/james
DELETE /users/james
のようにURLとHTTPメソッドを見れば何をするAPIなのかをイメージしやすい。
小規模アプリや単純なCRUD(Create-Read-Update-Delete)ならRESTで十分なことが多い。
RESTのデメリット
例えば、一覧画面ではid, nameだけほしいが、詳細画面ではid, name, email, postsもほしい。
その場合、RESTではAPIを複数作ったり、余計なデータを返したりしがち。
- オーバーフェッチ : 必要ないデータまで取ってしまう
- アンダーフェッチ : 必要なデータが足りず、追加でAPIを呼ぶ必要がある
GraphQLとは?
フロントエンド側がほしいデータの形を指定できるAPI方式
query {
user(id: 1) {
id
name
}
}
というGraphQLクエリをサーバーに送ったら、バックエンドは例えば以下のようなJSONを返す。
{
"data": {
"user": {
"id": 1,
"name": "James"
}
}
}
というように、emailを指定しなければ、emailを含めないでレスポンスを得ることができ、不要なデータを取得しないため、パフォーマンスが良い。
GraphQLのメリット
必要なデータだけを指定して取得できること
例えば一覧画面では、
query {
users {
id
name
}
}
詳細画面では、
query {
user(id: 1) {
id
name
email
posts {
id
title
}
}
}
のように、画面ごとに欲しいデータを変えられる。
GraphQLのデメリット
- 便利だが、RESTより覚えるものが多くて学習コストが高い
schema
type
query
mutation
resolver
input
fragment
Apollo Client
codegen
- フロント側が自由にデータを指定できる分、バックエンド側ではパフォーマンスに注意が必要
- 深い関連データを大量に取得すると、DBクエリが重くなることがある
- オーバーフェッチやN+1問題につながることもある
RESTとGraphQLの違い
| 項目 | REST | GraphQL |
|---|---|---|
| 基本の考え方 | URLとHTTPメソッドで操作する | クエリで欲しいデータを指定する |
| エントリーポイント |
Controllerで受け取る |
Resolverで受け取る |
| エンドポイント | 複数になりやすい | 基本は /graphql 1つ |
| データの形 | サーバー側が決める | フロント側が指定する |
| 取得 | GET |
Query |
| 作成・更新・削除 |
POST, (PUT, PATCH), DELETE
|
mutation |
| 柔軟性 | 普通 | 高い |
| 小規模アプリ | 向いている | 少し大げさな場合もある |
| 複雑な業務画面 | APIが増えやすい | 関連データをまとめやすい |
// REST APIのController
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller('hello')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
// GraphQL APIのResolver
import { Query, Resolver } from '@nestjs/graphql';
import { AppService } from './app.service';
@Resolver()
export class AppResolver {
constructor(private readonly appService: AppService) {}
@Query(() => String)
getHello(): string {
return this.appService.getHello();
}
}
Apolloとは?
=GraphQLをフロントエンド・バックエンドで扱いやすくするためのライブラリ群
ReactからGraphQLを使うときによく出てくる。
特に主要なライブラリがApollo ClientとApollo Serverの2つ。
Apollo Client : フロントエンド側で GraphQL API を呼ぶためのライブラリ
-
ReactからGraphQL APIを呼び出すために使う
以下のようにgqlを使って@Query()を定義し、import { gql, useQuery } from '@apollo/client' const GET_ITEMS = gql` query GetItems { items { id name } } `useQueryを使うことでGraphQLにおけるデータ取得の@Query()を実行できるconst { data, loading, error } = useQuery(GET_ITEMS)-
data: 取得できたデータ -
loading: 読み込み中かどうか -
error: エラー情報
同様に、以下のように
gqlを使うことで@mutation()を定義し、useMutationを使うことでGraphQLにおけるデータの追加・更新・削除の@mutation()を実行できるimport { gql, useMutation } from '@apollo/client' const CREATE_ITEM = gql` mutation CreateItem($name: String!) { createItem(input: { name: $name }) { id name } } ` export function AddItemButton() { const [createItem, { loading }] = useMutation(CREATE_ITEM) const handleClick = async () => { await createItem({ variables: { title: '勉強する', }, }) } } -
Apollo Clientの初期設定
ReactアプリでApollo Clientを使う時は、最初にクライアントを作る
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'
const client = new ApolloClient({
uri: 'http://localhost:3000/graphql',
cache: new InMemoryCache(),
})
そしてアプリ全体をApolloProviderで包むことで、アプリ内のどこでもuseQueryやuseMutationが使えるようになる
<ApolloProvider client={client}>
<App />
</ApolloProvider>
InMemoryCache=Apollo Clientのキャッシュ機能
GraphQLで取得したデータをメモリ上に保存しておける
例えば一度取得したデータを別の画面でまた使う場合に、毎回サーバーへ取りに行かずに済む!
ただ、キャッシュが効くことでDBでは更新されているのに画面が古いままということも起きる
→ fetchPolicyを設定する
fetchPolicy=データをキャッシュから読むか、毎回ネットワークから取るかを指定する
以下の場合は、キャッシュを使わず、毎回サーバーから取得する
const { data } = useQuery(GET_ITEMS, {
fetchPolicy: 'no-cache',
})
| fetchPolicy | 意味 |
|---|---|
cache-first |
まずキャッシュを見る。なければ通信する |
network-only |
毎回通信するが、結果はキャッシュに保存する |
no-cache |
毎回通信し、キャッシュにも保存しない |
cache-and-network |
キャッシュを即表示しつつ、裏で最新データも取りに行く |
network-only のキャッシュ保存結果は、以下のような場面で使われます。
- 他の
cache-first/cache-onlyのクエリで再利用できる - 同じエンティティを使う別画面・別コンポーネントの表示に使える
-
nextFetchPolicyで2回目以降cache-firstにしたときに使える - mutation後のキャッシュ更新などと連動できる
Apollo Server : バックエンド側で GraphQL API を作るためのライブラリ
NestJSを使う場合は、NestJSのGraphQL機能の裏側でApollo Serverを使うことが多い
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
})
ApolloDriverがNestJSでApollo系(Apollo以外にもう一つMercuriusがあるが、Fastify向けのため、今回は無視)のGraphQLサーバーを使うための設定
RESTならController、GraphQLならResolverを作るが、Resolverに来たGraphQLリクエストを処理するための土台としてApollo Server/Apollo Driverが使われている
GraphQL APIとApolloのまとめ
| 項目 | GraphQL | Apollo |
|---|---|---|
| 正体 | APIの仕様・問い合わせ言語 | GraphQLを使いやすくするライブラリ群 |
| 役割 | 「どういう形式でデータを取得・更新するか」を決める | GraphQLの通信・キャッシュ・状態管理などを便利にする |
| 例えるなら | ルール・言語 | そのルールを使うための道具 |
| フロントエンドでの役割 |
query や mutation の書き方を決める |
useQuery / useMutation でGraphQL APIを呼ぶ |
| バックエンドでの役割 | Schema / Query / Mutation / Type の構造を決める | Apollo ServerとしてGraphQL APIを動かす |
| Reactとの関係 | Reactから直接GraphQLを送ることもできる | ReactでGraphQLを扱いやすくするApollo Clientがよく使われる |
| NestJSとの関係 | NestJSでGraphQL APIを作れる |
ApolloDriver を使うと、NestJSのGraphQLサーバー実装としてApolloを使う |
| 必須か? | GraphQL APIを使うなら必要 | 必須ではない。Apollo以外に urql / Relay / Mercurius などもある |
| 代表的なコード | query { todos { id title } } |
const { data } = useQuery(GET_TODOS) |
Prismaとは?
TypeScript / Node.jsからDBを型安全に 操作するためのORM
→ NestJSのServiceなどからDBを直接SQLで操作する代わりに、Prismaを使ってDB操作を行う
-
ORM=Object Relational Mapping- プログラム上のオブジェクトとDBのテーブルを自動で繋ぐ技術・手法
- SQLを直接書く代わりにプログラミング言語の構文でDB操作ができるため、可読性が高い
Prismaの構成要素
1. Prisma Schema
schema.prismaというDBの構造を書くファイル
2. Prisma Client
実際にTypeScriptからDBを操作するためのもの
schema.prismaを書いた後に、
npx prisma generate
を実行すると、schema.prismaを元にPrisma Clientが生成される
3. Prisma Migrate
schema.prismaの変更をDBに反映する仕組み
npx prisma migrate dev --name マイグレーション名
を実行すると、PrismaがSQL migrationファイルを作って、schema.prismaの変更をDBに反映する
4. Prisma Studio
DBの中身をブラウザで確認・編集できるGUIツール
npx prisma studio
複雑なSQLやpgvectorのような特殊な検索では$queryRawを使い生のSQLを書くこともある
const results = await prisma.$queryRaw`
SELECT *
FROM documents
ORDER BY embedding <=> ${embedding}::vector
LIMIT 5
`
つまり、
普段のCRUD→PrismaClient
複雑なSQL→$queryRaw
Prismaの導入(Prisma7)
backendディレクトリで以下のコマンドを実行して、必要なパッケージをインストールする。
npm install prisma @prisma/adapter-pg pg @prisma/client
npm install -D @types/pg
-
@prisma/adapter-pg: Prisma7がPostgreSQLに接続するためのadapter -
pg: Node.js用のPostgreSQLドライバ -
@types/pg: TypeScript用の型定義 -
@prisma/client: PrismaClientの生成場所がoutputで指定した場所になったPrisma7だが、outputに生成されたgenerated/prisma/client.tsの内部では`import * as runtime from "@prisma/client/runtime/client"というruntimeを読んでいるため、@prisma/clientは必要
インストールが済んだら、以下のコマンドでPrismaの初期化する。
npx prisma init
このコマンドによって、Prisma 7の場合、backendディレクトリに以下が生成される。
backend/
├── prisma/schema.prisma
├── .env
├── prisma.config.ts
schema.prismaは以下のようになる。
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
}
-
provider = "prisma-client"- Prisma7推奨寄りの新しいPrisma Client生成方式
- 従来のPrisma6の
provider = "prisma-client-js"は@prisma/clientパッケージの中に生成する方式 - Prisma7 →
outputで指定した場所からimport - Prisma6 →
@prisma/clientパッケージからimport
prisma.config.tsは以下のようになっている。
スキーマの場所、migrationファイルの場所、dbのurlの指定を行っている。
import 'dotenv/config';
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: process.env['DATABASE_URL'],
},
});
最後に
普段使用している技術がどのようなものなのかについて、使用する前にも学習したが、ある程度使用した後に改めて学習すると、理解できることも多いと思います。以上です。