Help us understand the problem. What is going on with this article?

Next.jsとGo言語(gqlgen)でGraphQLを使ったアプリケーションを構築する方法

この記事では、フロントエンドにNext.js、バックエンドにGo言語(gqlgen)を用いて、フロントエンドとバックエンド間のAPIにGraphQLを使ったアプリケーションを構築する方法をまとめます。

作成したのは以下のような簡易ブクマアプリです。
image.png
フロントエンドはほぼNext.jsのexamplesのままで、本記事ではバックエンドの実装をメインに解説します。

背景

個人的に、「フロントエンドフレームワークとGo言語の組み合わせで何か開発をしてみたい」と以前から思っていました。
そんな中、ReactのフレームワークであるNext.jsの使い勝手が良さそうという噂を耳にしました。

加えて、Next.jsのexamplesが充実していて、Apolloを使ったアプリケーションのひな形が簡単に作れることを知ったので、今回Go言語のバックエンドと組み合わせて動かしてみることにしました。

構成要素

図に書いてみると思っていた以上に構成要素が多かったので、それぞれの役割を大まかに説明します。
Untitled_LINE_Beacon_-_Cacoo.png

名前 種別 役割
JavaScript プログラミング言語 今回のフロントエンドの実装に用いるプログラミング言語
React ライブラリ コンポーネントベースでUIを構築できるJavaScriptライブラリ
Apollo Client ライブラリ GraphQLに対応した状態管理ライブラリ
Next.js フレームワーク Reactのサーバーサイドレンダリング(SSR)に対応するフレームワーク
GraphQL クエリ言語/ランタイム API向けに作られたクエリ言語およびランタイム
Go言語 プログラミング言語 今回のバックエンドの実装に用いるプログラミング言語, golangと表記されることもある
gqlgen ライブラリ SchemaベースでGraphQLサーバを構築するためのライブラリ

UIの構築

まずはNext.jsのexamples/with-apolloをベースにアプリケーションを作ります。

$ yarn create next-app
success Installed "create-next-app@9.4.4" with binaries:
      - create-next-app
✔ What is your project named? … with-apollo-ui
✔ Pick a template › Example from the Next.js repo
✔ Pick an example › with-apollo
Creating a new Next.js app in /Users/yokazaki/src/github.com/yuuu/with-apollo-ui.

# ログ省略

$ cd with-apollo-ui
$ yarn dev

出来上がったアプリケーションは、URLとタイトルをセットで登録・閲覧できる、いわゆる簡易ブクマアプリです。

image.png

この時点では、リクエストの送信先となっているバックエンドのGraphQLサーバはインターネット上に公開されているものを利用しています。
このため、列挙されているURLとタイトルは、世界中のユーザが登録したものがそのまま表示されています。

GraphQLサーバの構築

スキーマベースでGraphQLサーバを構築できることと、機能の拡張性を考慮して、以下記事を参考にEcho+gqlgenを使って構築しました。

gqlgen + EchoでgolangなGraphQLサーバを作るチュートリアル

ベース構築

$ mkdir with-apollo-api
$ cd with-apollo-api
$ go mod init github.com/yuuu/with-apollo-api
$ go get github.com/99designs/gqlgen
$ go get github.com/rs/cors

# gqlgenでgraph/schema.graphqlsを生成
$ gqlgen init

スキーマ定義

graph/schema.graphqls にGraphQLのスキーマを記述します。

graph/schema.graphqls
type Post {
  id: ID!
  title: String!
  votes: Int!
  url: String!
  createdAt: String!
}

type PostsMeta {
  count: Int!
}

type Query {
  allPosts(orderBy: OrderBy, first: Int!, skip: Int!): [Post!]!
  _allPostsMeta: PostsMeta!
}

enum OrderBy {
  createdAt_ASC,
  createdAt_DESC
}

type Mutation {
  createPost(title: String!, url: String!): Post!
  updatePost(id: ID!, votes: Int): Post!
}

スキーマを記述したらソースコードを生成します。

$ rm graph/schema.resolvers.go
$ gqlgen

QueryとMutationを実装

生成された graph/schema.resolvers.go を以下のように変更します。

graph/schema.resolvers.go
package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
    "context"
    "fmt"
    "sort"
    "strconv"
    "time"

    "github.com/yuuu/with-apollo-api/graph/generated"
    "github.com/yuuu/with-apollo-api/graph/model"
)

var posts []*model.Post = make([]*model.Post, 0)

func (r *mutationResolver) CreatePost(ctx context.Context, title string, url string) (*model.Post, error) {
    post := model.Post{
        ID:        fmt.Sprintf("%d", len(posts)+1),
        Title:     title,
        URL:       url,
        Votes:     0,
        CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
    }
    posts = append(posts, &post)
    return &post, nil
}

func (r *mutationResolver) UpdatePost(ctx context.Context, id string, votes *int) (*model.Post, error) {
    if votes == nil {
        return nil, nil
    }
    i, _ := strconv.Atoi(id)
    posts[i-1].Votes = *votes
    return posts[i-1], nil
}

func (r *queryResolver) AllPosts(ctx context.Context, orderBy *model.OrderBy, first int, skip int) ([]*model.Post, error) {
    if skip > len(posts) {
        skip = len(posts)
    }
    if (skip + first) > len(posts) {
        first = len(posts) - skip
    }
    sortedPosts := make([]*model.Post, len(posts))
    copy(sortedPosts, posts)
    if orderBy != nil && *orderBy == "createdAt_DESC" {
        sort.SliceStable(sortedPosts, func(i, j int) bool {
            return sortedPosts[i].CreatedAt > sortedPosts[j].CreatedAt
        })
    }
    slicePosts := sortedPosts[skip : skip+first]
    return slicePosts, nil
}

func (r *queryResolver) AllPostsMeta(ctx context.Context) (*model.PostsMeta, error) {
    postsMeta := model.PostsMeta{Count: len(posts)}
    return &postsMeta, nil
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

Playgroundで動作確認

以下コマンドでGraphQLサーバを起動します。

$ go run server.go

ブラウザで http://localhost:8080 にアクセスすると、PlayGroundが表示されます。

まずはmutationから動作確認してみましょう。
Yahoo! JAPANを登録してみます。
image.png
次にGoogleを登録してみます。
image.png
続いてQueryを試してみます。
image.png
このように、GraphQLの各リクエストが問題なく動作していることがわかります。

UIとGraphQLサーバを組み合わせる

CORS設定

現状、Next.jsが動いているオリジン(http://localhost:3000) とGraphQLサーバのオリジン(http://localhost:8080) が異なるため、このままではGraphQLサーバへのリクエストが失敗します。

server.goを以下のように変更することで、 http://localhost:3000 からの要求を受け付けられるようにします。

server.go
package main

import (
    "log"
    "net/http"
    "os"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/playground"
    "github.com/rs/cors"
    "github.com/yuuu/with-apollo-api/graph"
    "github.com/yuuu/with-apollo-api/graph/generated"
)

const defaultPort = "8080"

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }

    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))

    http.Handle("/", playground.Handler("GraphQL playground", "/query"))

    c := cors.New(cors.Options{
        AllowedOrigins:   []string{"http://localhost:3000", "http://localhost:8080"},
        AllowCredentials: true,
    })

    http.Handle("/query", c.Handler(srv))

    log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

Next.jsのリクエスト先のURLを変更

lib/apolloClient.js のServer URLを http://localhost:8080/query に変更します。

lib/apolloClient.js
import { useMemo } from 'react'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'

let apolloClient

function createApolloClient() {
  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
    link: new HttpLink({
      uri: 'http://localhost:8080/query', // Server URL (must be absolute)
      credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
    }),
    cache: new InMemoryCache(),
  })
}

export function initializeApollo(initialState = null) {
  const _apolloClient = apolloClient ?? createApolloClient()

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // gets hydrated here
  if (initialState) {
    _apolloClient.cache.restore(initialState)
  }
  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient

  return _apolloClient
}

export function useApollo(initialState) {
  const store = useMemo(() => initializeApollo(initialState), [initialState])
  return store
}

動作確認

UIとGraphQLサーバをともに起動します。

$ cd with-apollo-ui # 移動先パスは適宜変更ください
$ yarn dev

# 以下は別のterminalで
$ cd with-apollo-api # 移動先パスは適宜変更ください
$ go run server.go

http://localhost:3000 へアクセスすると、URLの追加や投票が正常に動作することが確認できます。
image.png

まとめ

Next.jsのサンプルが充実しているおかげで、簡単にアプリケーションを構築できました。これに認証やバリデーションを追加して、UIを自分好みにカスタマイズすれば簡単にサービスをリリースできそうです。

Next.js・Go言語ともにもっと事例が増えると良いなと思っています。
みなさまも、ぜひお試しください。

Y_uuu
fusic
個性をかき集めて、驚きの角度から世の中をアップデートしつづける。
https://fusic.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした