Nextjs(custom server) +GraphQL+postgresqlでRSCを試してみる
前提
このあたりの技術要素を扱います。
-
Nextjs
- CustomServer
- AppRouter
-
Nodejs
- Express
-
GraphQL
- Apollo
- Neon(Postgresql)
- DrizzleORM
NextjsはAppRouterでCustomServerとしています。RSC(React Server Component)を試してみたいのと、CustomServerはGraphQLサーバーとクライアント両方をNextjsに持たせてみたいという意図で選定しました。
データベースはNeon(Postgresql)を選定しました。FreePlanで個人利用する分にはこちらが最適そうだったので選びました。
ORMはDrizzleを選定しました。GitHubリポジトリのStarスターの伸び具合を表示するサイトを見ると将来性がありそうだったので選定しました。
やりたいこと
Nextjs(custom server)でNeon(postgresql)に接続してみます!。
手順
1.Nextjsの初期設定
npxで初期化していきます。
typescript,tailwind,AppRouterを選択します。
npx create-next-app@latest .
create-next-app@14.2.5
Ok to proceed? (y) y
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Creating a new Next.js app in /home/****
http://localhost:3000が起動することを確認したら次に進みます
npm run dev
2.CustomServerを設定する
NextjsはNodejs上で動作していますが、Nodejs部分をより自由にカスタマイズできるようにCustomServerの設定をします。
ts-node,@types/expressをインストールします。
npm i -D ts-node @types/express
expressもインストールします。
npm i express
server/index.tsを作成します。
expressを起動し、'*'でnextjs用のハンドラオブジェクトをreturnするようにします。
※これでnextjsコンテンツを返すようになります。
import type { Request, Response } from 'express'
import express from 'express'
import next from 'next'
const dev = process.env.NODE_ENV !== 'production'
const nextApp = next({ dev, isNodeDebugging: true })
const handle = nextApp.getRequestHandler()
const port = process.env.PORT || 8080
async function main() {
try {
await nextApp.prepare()
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.all('*', (req: Request, res: Response) => {
return handle(req, res)
})
app.listen(port, (err?: unknown) => {
if (err) throw err
console.log(`> Ready on localhost:${port} - env ${process.env.NODE_ENV}`)
})
} catch (e) {
console.error(e)
process.exit(1)
}
}
main()
次にルートにtsconfig.build.jsonを作成します。
build時にこちらの追加オプションが必要になります。
{
"extends": "./tsconfig.json", // tsconfig.jsonの設定を継承する
"compilerOptions": {
"module": "CommonJS", // Next.jsとExpressの両方を連携させるために、commmonjsを利用する
"outDir": "dist", // ビルドファイルの出力先
"isolatedModules": false,
"noEmit": false // Next.jsはBebelを使用してTypeScriptをコンパイルするので、TSコンパイラはjsを出力しない。設定を上書きする。
},
"include": ["server"], // TSコンパイラにserverディレクトリのみをコンパイル対象として認識させる。
"exclude": ["node_modules"]
}
最後にpackage.jsonのscriptsと.vscode/launch.jsonを以下のように修正して終わりです。launch.jsonはデバッグ実行のためだけの設定です。
package.json
"scripts": {
"dev": "ts-node --project tsconfig.build.json server/index.ts",
"build:next": "next build",
"build:server": "tsc --project tsconfig.build.json",
"build": "npm run build:next && npm run build:server",
"start": "next start",
"lint": "next lint"
},
launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**"
]
},
]
}
以上でCustomServerの設定は終わったのでデバッグしてみましょう
vscodeでserver/index.tsのapp.allのところにブレークポイントを張って
デバッグ実行して止まることを確認しましょう。
3.Nextjs(サーバー側)からNeon(Postgresql)に接続してみる
Neonを利用するにはこちらの解説に沿ってアカウント登録等を行っていきます。
環境変数を利用するためにdotenvを導入します。
npm i -D dotenv
server/index.tsで以下のようにdotenvをimportすることで.envファイルを環境変数として読み込めるようにします。
import 'dotenv/config'
.envには以下DB接続文字列をNeon(Postgresql)からコピペしてきます。
DATABASE_URL=postgresql://******:******@*******.ap-southeast-1.aws.neon.tech/********?sslmode=require
DBアクセスするためのパッケージとORMをインストールします。
npm i @neondatabase/serverless drizzle-orm
※react:^18 だとなぜかdrizzle-ormでdependencyエラーとなったので下記18.3.1を入れなおして上記を再実行しました。
npm i react@^18.3.1 react-dom@^18.3.1
DBアクセスするためのコードを書いていきます。
server/db/index.tsに以下記述します。
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql)
server/index.ts でdbインポートして接続できているか試しに確認してみます。
import type { Request, Response } from 'express'
import express from 'express'
import next from 'next'
import 'dotenv/config'
import { db } from './db'
const dev = process.env.NODE_ENV !== 'production'
const nextApp = next({ dev, isNodeDebugging: true })
const handle = nextApp.getRequestHandler()
const port = process.env.PORT || 8080
async function main() {
try {
await nextApp.prepare()
const app = express()
const dbInstance = await db
console.log(dbInstance)
app.use(express.json())
セッションは作られてそうです。
DBに接続できました。
4.Neon(Postgresql)にテーブル作成
DrizzleORMを導入したので、ローカルにあるスキーマからテーブル作成が可能となります。
Drizzleのスキーマを定義していきます。
server/db/model/index.tsにスキーマを定義します。
ToDoテーブルを作成します。
import {
pgTable,
varchar,
} from 'drizzle-orm/pg-core'
export const user = pgTable('todo', {
taskId: varchar('taskId', { length: 256 }).primaryKey(),
contents: varchar('contents', { length: 256 }),
})
DrizzleがローカルからNeon(Postgresql)に対してSQL文を発行できるようにconfigを設定していきます。
まずはDrizzle-Kitをインストールします。
npm i -D drizzle-kit
次にdrizzle.config.tsをルートに作成します。
その他のDBの設定はこちらに記載があります。
import type { Config } from 'drizzle-kit'
import * as dotenv from 'dotenv'
dotenv.config()
export default {
schema: './server/db/model/index.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL || '',
},
verbose: true,
strict: true,
} satisfies Config
package.jsonにdrizzleコマンドを追加します。
"scripts": {
"dev": "ts-node --project tsconfig.build.json server/index.ts",
"build:next": "next build",
"build:server": "tsc --project tsconfig.build.json",
"build": "npm run build:next && npm run build:server",
"start": "next start",
"lint": "next lint",
"drizzle:generate": "drizzle-kit generate --config=./drizzle.config.ts",
"drizzle:push": "drizzle-kit push --config=./drizzle.config.ts",
"drizzle:introspect": "drizzle-kit introspect --config=./drizzle.config.ts"
},
- ①generateはローカル(drizzle.config.tsでoutに指定したフォルダ)にSQL文を作成します。
- ②pushは生成したSQL文をDBに反映します。
- ③introspectはDBからSQL文を生成します。
①②を順番に実行することでスキーマで指定したToDoテーブルをDBに反映することができます。実行してみましょう。
npm run drizzle:generate
> bookzero@0.1.0 drizzle:generate
> drizzle-kit generate --config=./drizzle.config.ts
Reading config file '/home/hiro/bookzero/drizzle.config.ts'
1 tables
todo 2 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ drizzle/0000_thin_boom_boom.sql 🚀
SQLが作成されています。
次にpushします。
※すでにDBに他のテーブルがneonに存在している場合は、introspectを行って作成されたスキーマをローカルコードに反映してからgenerate,pushするようにしましょう。
npm run drizzle:push
[✓] Changes applied
Neonを確認するとテーブルが作成されていることがわかります。
5.バックエンドGraphQLの設定
GraphQLサーバーとして@apollo/serverをインストールします。
npm i @apollo/server
ApolloServerのインスタンスに必要な情報を定義していきます。
server/graphql/index.tsに作成します。
apolloServerは、
- typeDefs:型
- resolver:解決(どのような値を返すかプログラムする)
をinputとしますので以下のように作成します。
import type { BaseContext } from '@apollo/server'
import { ApolloServer } from '@apollo/server'
import type { NeonHttpDatabase } from 'drizzle-orm/neon-http'
import ToDo from './model/todo'
class GraphQL {
private todo: ToDo
constructor(db: NeonHttpDatabase<Record<string, never>>) {
this.todo = new ToDo(db)
}
private typeDefs = `
type Todo {
taskId: String!
contents: String
}
type Query {
getTodos: [Todo]
}
`
private resolvers = {
Query: {
getTodos: async (root: unknown, {}, {}) => {
return this.todo.getToDos()
},
},
}
public apolloServer = new ApolloServer<BaseContext>({
typeDefs: this.typeDefs,
resolvers: this.resolvers,
})
}
export default GraphQL
ToDoモデルを作ってDBの値を取得するようにします。
server/graphql/model/todo.ts
import type { NeonHttpDatabase } from 'drizzle-orm/neon-http'
import { todo } from '../../db/model'
export type GetTaskInputType = {
input: {
taskId: string
}
}
class ToDo {
private db
constructor(db: NeonHttpDatabase<Record<string, never>>) {
this.db = db
}
getToDos = async () => {
const res = await this.db
.select()
.from(todo)
return res
}
}
export default ToDo
ExpressのミドルウェアにGraphQLを起動するように追加します。
server/index.ts
import type { Request, Response } from 'express'
import { expressMiddleware } from '@apollo/server/express4'
import express from 'express'
import next from 'next'
import 'dotenv/config'
import { db } from './db'
import GraphQL from './graphql'
const dev = process.env.NODE_ENV !== 'production'
const nextApp = next({ dev, isNodeDebugging: true })
const handle = nextApp.getRequestHandler()
const port = process.env.PORT || 8080
async function main() {
try {
/**
* GraphQLクラス定義
*/
const graphQl = new GraphQL(db)
await graphQl.apolloServer.start()
await nextApp.prepare()
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
/**
* GraphQL(ApolloServer)のミドルウェア
*/
app.use(
'/graphql',
expressMiddleware(graphQl.apolloServer),
)
app.all('*', (req: Request, res: Response) => {
return handle(req, res)
})
app.listen(port, (err?: unknown) => {
if (err) throw err
console.log(`> Ready on localhost:${port} - env ${process.env.NODE_ENV}`)
})
} catch (e) {
console.error(e)
process.exit(1)
}
}
main()
この状態でデバッグを起動したら
http://localhost:8080/graphqlを参照してみましょう。
以下が表示されるはずです。
クエリを実行しても空の配列が取得されるのはテーブルにレコードがないからです。
2レコード追加しました。
クエリを実行したら2レコード表示されていました。
6.フロントエンドGraphQLの設定
下準備としてgraphql-codegenの設定をしていきます。
graphql-codegen:GraphQL Code Generatorは、GraphQLスキーマからコードを生成するツールです。
必要なパッケージを追加します。
npm i -D @graphql-codegen/cli @graphql-codegen/client-preset @graphql-codegen/introspection
次にルートにcodegen.tsを作成します。
schemaにはgraphqlのエンドポイント、generatesには生成される型のパスを指定します。
フロントエンドはsrc配下なのでそちらを指定します。
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
overwrite: true,
schema: 'http://localhost:8080/graphql',
documents: '**/*.{gql,graphql}',
generates: {
'src/graphql/generated/': {
preset: 'client',
plugins: [],
},
'./graphql.schema.json': {
plugins: ['introspection'],
},
},
}
export default config
次にスキーマファイルを作成します。
src/graphql/todo.query.gql
query GetTodos {
getTodos {
taskId
contents
}
}
package.jsonのscriptsにcodegenを追加します。
"scripts": {
"dev": "ts-node --project tsconfig.build.json server/index.ts",
"build:next": "next build",
"build:server": "tsc --project tsconfig.build.json",
"build": "npm run build:next && npm run build:server",
"start": "next start",
"lint": "next lint",
"drizzle:generate": "drizzle-kit generate --config=./drizzle.config.ts",
"drizzle:push": "drizzle-kit push --config=./drizzle.config.ts",
"drizzle:introspect": "drizzle-kit introspect --config=./drizzle.config.ts",
"codegen": "graphql-codegen --config codegen.ts"
},
実行してみましょう。
まずはnpm run devでgraphqlを起動した状態にしておいて、以下を実行します。
npm run codegen
> ****@0.1.0 codegen
> graphql-codegen --config codegen.ts
✔ Parse Configuration
✔ Generate outputs
成功したら型定義ファイルが生成されているはずです。
7.フロントエンドからGraphQLを呼び出す
apollo clientをインストールします
npm i @apollo/client
RSCでapollo clientを試したいので以下インストールします
npm i @apollo/experimental-nextjs-app-support
apollo clientを呼び出すtsを作成します。
src/lib/apollo/apolloClient.ts
import { HttpLink } from '@apollo/client'
import {
ApolloClient,
InMemoryCache,
registerApolloClient,
} from '@apollo/experimental-nextjs-app-support'
export const { getClient, query, PreloadQuery } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: `http://localhost:8080/graphql`,
fetchOptions: { cache: 'no-store' },
}),
})
})
page.tsxで動的にToDoコンポーネントを呼び出し、
ToDoコンポーネントではGraphQLリクエストするようにします。
なぜか export const dynamic = 'auto' としてしまうと
動的レンダリング判定とならずエラーとなってしまいます・・・!
src/app/page.tsx
export const dynamic = 'force-dynamic'
import { ToDo } from '@/app/components/ToDo'
import { Suspense } from 'react'
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
<Suspense fallback={<>loading</>}>
<ToDo />
</Suspense>
</div>
</main>
)
}
src/app/components/ToDo.tsx
import { GetTodosDocument } from '@/graphql/generated/graphql'
import { getClient } from '@/lib/apollo/apolloClient'
export async function ToDo() {
const { data } = await getClient().query({ query: GetTodosDocument })
return (
<div>
<div>data:{JSON.stringify(data)}</div>
</div>
)
}
ローカル実行時、ローカルビルド時、本番ビルド時すべて問題ないようにpackage.jsonを修正します。
"scripts": {
"dev": "ts-node --project tsconfig.build.json server/index.ts",
"build:server": "tsc --project tsconfig.build.json",
"build:next": "next build",
"build": "npm run build:server && npm run build:next",
"local": "NODE_ENV=development ts-node --project tsconfig.build.json dist/index.js",
"start": "NODE_ENV=production ts-node --project tsconfig.build.json dist/index.js",
"lint": "next lint",
"drizzle:generate": "drizzle-kit generate --config=./drizzle.config.ts",
"drizzle:push": "drizzle-kit push --config=./drizzle.config.ts",
"drizzle:introspect": "drizzle-kit introspect --config=./drizzle.config.ts",
"codegen": "graphql-codegen --config codegen.ts"
},
ビルド&実行してみます。
npm run build
▲ Next.js 14.2.5
- Environments: .env
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (5/5)
✓ Collecting build traces
✓ Finalizing page optimization
Route (app) Size First Load JS
┌ ƒ / 17.7 kB 105 kB
└ ○ /_not-found 871 B 87.9 kB
+ First Load JS shared by all 87 kB
├ chunks/23-0400b34f6f7b0912.js 31.5 kB
├ chunks/fd9d1056-d26b0eab797fab5a.js 53.6 kB
└ other shared chunks (total) 1.86 kB
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
動的(Dynamic)にbuildされてそうです。
npm run start
loading表示後に以下Jsonが表示されました。
感想
AppRouterで動的なページを強制するには'use client'を削除すればいいだけだと思っていましたが
export const dynamic = 'force-dynamic'
としなければいけなかったみたいで少しハマりました・・・。NextjsをCustomServerで設定するメリットは自然な形でNodejs×Express×GraphQLを導入できるところかなぁと思っています。
関連
リポジトリ
リポジトリ
https://github.com/webshoten/nextjs_customserver_graphql
Discussion