31
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Next.js + NestJS で完全TypeScriptでSSR してHerokuにデプロイする方法を誰よりもわかりやすく説明します (モノレポ化)

Last updated at Posted at 2022-02-16

はじめに

どうもyoshiiです。
今まで、いろんなwebアプリ作ってきたんですが、どれもフロントエンドとバックエンドを別サーバーに置いてSPAで実装するばかりで、たまにはSPA以外もやってみたいなと思ったのが発端でした。

ちなみに今まで作ったアプリとかは大体こちらにまとめてます。基本的になんのありがたみもないようなものを作っています。
暇人は見てね。

やることはタイトルの通りです。
1つのプロジェクト内でNext.jsとNestJSを配置し、
フロントエンドとバックエンドをそれぞれ担当させ、1つのサーバーに配置し、SSR(サーバーサイドレンダリング)まで実装します。
正直、今までwebアプリ作ったことないような人でも、愚直にこの記事通りやればwebアプリの雛形が作れると思いますので、そういう人も是非是非読んでみてください。
質問も答えられる範囲であれば、答えます!

イメージとしては、yarn startでNestJSサーバーが立ち上がり、サーバーにアクセスしたらNext.jsで実装したフロントエンドがレンダリングされるって感じです。

ちなみに、Next.js公式はSSRより圧倒的にSSGを推奨していますが、サーバーで複雑な動きさせたいってなるとNestJSのプロジェクトにNextを配置するような形が良いとも思います。
なので、とにかく色々作りたいタイプには、まずは汎用性の高いSSRからやるのをおすすめします。

SSRはパフォーマンスがSSGよりも落ちる代わりに、SSRさえできればどんな形のアプリにでもオールマイティに対応できると思います。
またSSRはSEO面も良い(検索した時に上の方に出てきやすい)です。

それに、このNext.jsでSSRを実装したらちょっとビルド方法変えて関数名変えたらSSGもできるので、SSGしたい人も参考になるかもしれません。

目次

前提知識

Next.jsとかNestJSとかSSRとか、そもそもなんだそれはって人向けに筆者の曖昧な知識で解説します。

- React: webアプリの見えてる部分(フロントエンド)の開発をスマートにできる部品の集まり。(ライブラリ)
- Next.js: Reactをさらにスマートにして、できることも増やしたやつ。React用フレームワーク。
- NestJS: webアプリの見えない部分(バックエンド)の開発をスマートにできる雛形。(フレームワーク)
- SSR: 各ページにアクセス(遷移)するたびにサーバー側にリクエストが飛ぶような方式。よくわかんない人はスルーしていいです。
- TypeScript: 言語。JavaScriptを静的型付け言語に拡張したもの。安全に開発しやすい。
- heroku: 無料枠が優秀なPaaS。NestJSを簡単に動かせるサーバー。ここにデプロイ(公開)するので、アカウントを作っておいてください。
- モノレポ: フロントエンドとバックエンドを1個のリポジトリにまとめて、1つのコマンドで起動するようなプロジェクト(だと思っています)。
- yarn: nodejsのパッケージマネージャー。jsで作られたライブラリ(機能の部品)を管理するやつ。
    - npmでもいいですが、個人的にyarnが好きなのでよくわかんない人もとりあえず入れてください。
    - Homebrewってやつでnodenvとかanyenvとかを入れて、npmからyarnを入れるのが普通かな…?

こんなもんでしょうか。わかんない言葉あったら質問ください。多分俺もわかんないですが、一緒に考えましょう。質問がくるたびにここが増えていくような気がします。

構成とか

PC: macOS Big Sur
nodeのversion: v14.17.3
パッケージマネージャー: yarn

バックエンド: NestJS
フロントエンド: Next.js
言語: TypeScript
サーバー: Heroku

GitHub

手順とかどうでもいいからテンプレ見せろって感じなら、俺の公開してるリポジトリ見てください。

参考にしたもの

正直、実装に関しては以下の記事ほぼそのままです。
deepLさんで和訳しながら実装しました。

実装

では実装に入っていきます!

プロジェクトの作成

まずはプロジェクトを作ります。

nest cliを入れておきましょう。
NestJS公式を見て入れようね。

ターミナルで以下を実行

nest new nest-next-template

この時のnest-next-templateってとこは好きなプロジェクトの名前にしてね。
npmにするかyarnにするか聞かれるので好きな方選んでね。(筆者はyarn派です)

今回、テストはちょっと考えないことにしたいので、nest cliによって勝手に作られるsrc/testディレクトリと、src/app.controller.spec.ts は削除しておきましょう。

そして、NestJSプロジェクトが作成されたら、srcディレクトリの中にpages, server, shared ディレクトリを作りましょう。

現状srcディレクトリの中身はこんな感じ

src/
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
├── pages/
├── server/
└── shared/

〜 各ディレクトリの解説 〜

- pages/: Next.jsのpagesに相当するファイル群をここに置いていく
- server/: NestJSのファイル群をここに置いていく
- shared/: Next.jsとNestJSで共通の情報をここに配置する。型定義ファイルとかが多い。

なので、まずはsrc/serverディレクトリにnest cliで作成されたファイルたちを移動しましょう。

移動した後はこんな感じ

src/
├── pages
├── server
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
└── shared

次に、nest-cli.json ファイルを以下のように編集。

nest-cli.json
{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "entryFile": "main"
}

これでプロジェクトの初期化はとりあえず終わり。

各種ライブラリの導入

Next.js関連のライブラリを入れます。
ターミナルで以下を入力してください。

yarn add next react react-dom
yarn add -D @types/react @types/react-dom eslint-config-next

上記に加えて先で必要になるライブラリも今のうちに入れておきましょう
ターミナルで以下を入力。

yarn add nest-next axios

現状必要なライブラリはこんなものですかね。
Reactはフレームワークではなくライブラリなので、こんな感じで、最小構成のフロントエンド開発環境をNestJSプロジェクトに簡単に追加できたりします。(どこかで読んだ話ですが…)

ここから本格的に実装に入っていきましょう。

Next.jsの実装

フロントエンドの実装に入る前に、tsconfigファイルを用意してあげましょう。
ルートディレクトリにtsconfig.server.jsonを作成します。

tsconfig.server.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noEmit": false
  },
  "include": [
    "./src/server/**/*.ts",
    "./src/shared/**/*.ts",
    "./@types/**/*.d.ts"
  ]
}

また、ルートディレクトリに存在しているtsconfig.jsonを、以下のように編集します。

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

その後、package.jsonのscriptsの部分を以下のように書き換えます。これでyarn buildでNext.jsとNestJS両方のビルドが行われたり、とにかく上手いことやってくれるようになります。

package.json
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "yarn build:next && yarn build:nest",
    "build:next": "next build",
    "build:nest": "nest build --path ./tsconfig.server.json",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "node dist/server/main.js",
    "start:prod": "node dist/server/main.js",
    "start:next": "next dev",
    "start:dev": "nest start --path ./tsconfig.server.json --watch",
    "lint": "eslint \"{src,apps,libs,test}/**/*.tsx?\" --fix"
  },

よくわからん人は、とりあえずpackage.jsonに以下の内容をコピペしたってください。

package.json
{
  "name": "nest-next-template",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "yarn build:next && yarn build:nest",
    "build:next": "next build",
    "build:nest": "nest build --path ./tsconfig.server.json",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "node dist/server/main.js",
    "start:prod": "node dist/server/main.js",
    "start:next": "next dev",
    "start:dev": "nest start --path ./tsconfig.server.json --watch",
    "lint": "eslint \"{src,apps,libs,test}/**/*.tsx?\" --fix"
  },
  "dependencies": {
    "@nestjs/common": "^8.0.0",
    "@nestjs/core": "^8.0.0",
    "@nestjs/platform-express": "^8.0.0",
    "axios": "^0.26.0",
    "nest-next": "^9.6.0",
    "next": "^12.0.10",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0"
  },
  "devDependencies": {
    "@nestjs/cli": "^8.0.0",
    "@nestjs/schematics": "^8.0.0",
    "@nestjs/testing": "^8.0.0",
    "@types/express": "^4.17.13",
    "@types/jest": "^27.0.1",
    "@types/node": "^16.0.0",
    "@types/react": "^17.0.39",
    "@types/react-dom": "^17.0.11",
    "@types/supertest": "^2.0.11",
    "@typescript-eslint/eslint-plugin": "^4.28.2",
    "@typescript-eslint/parser": "^4.28.2",
    "eslint": "^7.30.0",
    "eslint-config-next": "^12.0.10",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^3.4.0",
    "jest": "^27.0.6",
    "prettier": "^2.3.2",
    "supertest": "^6.1.3",
    "ts-jest": "^27.0.3",
    "ts-loader": "^9.2.3",
    "ts-node": "^10.0.0",
    "tsconfig-paths": "^3.10.1",
    "typescript": "^4.3.5"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}

package.jsonを編集した後は、一応ターミナルでyarn installしておきましょうか。
豆知識ですが、yarn installyarnと入力するだけで実行されます。どうでもいいですね。

次に、src/pages/以下に_app.tsxindex.tsx、2つのファイルを追加

src/pages/_app.tsx
import type { AppProps } from 'next/app';

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default MyApp;
src/pages/index.tsx
import type { NextPage } from 'next';

const Home: NextPage = () => {
  return <h1>Home</h1>;
};

export default Home;

この時点で、ターミナルにyarn start:nextと入力して http://localhost:3000/ にアクセスしてやると、Homeって出てくると思います。

あと、.gitignoreにNext.jsのビルドディレクトリを追加しましょう。

.gitignore
# compiled output
/dist
/node_modules
/.next

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# OS
.DS_Store

# Tests
/coverage
/.nyc_output

# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

サーバーサイドの編集

ここからサーバーサイド(NestJS)のファイルを編集していきます。

まずは、main.ts。デプロイした時にポート番号を入力しなくて良くなるようにします。

src/server/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT || 3000);
}
bootstrap(); 

次に、app.module.ts を編集。nest-nextライブラリを使って、Next.jsをレンダリングできるようにします。dev: trueを指定すると、ファイルの更新を監視して変更してくれます。(ホットリロード的な?)

src/server/app.module.ts
import { Module } from '@nestjs/common';
import { RenderModule } from 'nest-next';
import Next from 'next';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [RenderModule.forRootAsync(Next({ dev: true }), { viewsDir: null })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

最後に、app.controller.ts を編集。@Renderデコレータを使って、index.tsxをレンダリングします。

src/server/app.controller.ts
import { Controller, Get, Render } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @Render('index')
  home() {
    return {};
  }
}

ここまでできたら、一度yarn start:devとターミナルに入力してみてください。
ルートディレクトリにNext.jsとNestJSのビルドディレクトリがそれぞれ生成され、NestJSのサーバーが起動します。
http://localhost:3000/ にアクセスすると、index.tsxがレンダリングされ、Homeって表示されると思います。

とりあえずここまででcommitを載せておきます。

サーバーサイドからデータを受け取って表示してみる

まず、型定義ファイルsrc/shared/types/index.d.tsを作成し、Story型を作ります。

src/shared/types/index.d.ts
export type Story = {
  id: number;
  title: string;
  description: string;
};

とりあえず、データベース用意するの面倒なので、データはapp.service.ts内に定数として直接持たせちゃいましょう。
本来はデータベースからfetchしてくると思ってください。

src/server/app.service.ts
import { Injectable } from '@nestjs/common';
import { Story } from 'src/shared/types';

const stories: Story[] = [
  {
    id: 1,
    title: '国歌',
    description: '首相が豚と…',
  },
  {
    id: 2,
    title: '1500万メリット',
    description: '超管理社会',
  },
  {
    id: 3,
    title: '人生の軌跡のすべて',
    description: '記憶をデータとして正確に閲覧できる世界の話',
  },
];

@Injectable()
export class AppService {
  getStories() {
    return stories;
  }
}

最近ネトフリで見て面白かった「ブラック・ミラー」というドラマシリーズのタイトルとあらすじを3つ用意してみました。(関係ないですがとてもおすすめのドラマです)

次に、app.controller.tsでサービスを呼び出して、Storyの配列を返すAPIを定義しましょう。

src/server/app.controller.ts
import { Controller, Get, Render } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @Render('index')
  home() {
    return {};
  }

  @Get('/api/stories')
  public getStories() {
    return this.appService.getStories();
  }
}

では、Story配列をフロントエンドで受け取って表示してみましょう。

まず、axiosを使ってフロントエンドとバックエンドの通信を行うのですが、ベースURLを省略したいのと、application/json形式で通信したいので、それをラップしてexportするファイルを用意します。

src/shared/lib/apiClient.tsファイルを以下のように作成します。

src/shared/lib/apiClient.ts
import axios from 'axios';

export const apiClient = axios.create({
  baseURL: 'http://localhost:3000/',
  responseType: 'json',
  headers: {
    'Content-Type': 'application/json',
  },
});

そして、src/pages/index.tsxgetServerSideProps 関数を用意して、サーバーサイドでAPIを叩きましょう。

src/pages/index.tsx
import type { GetServerSideProps, NextPage } from 'next';
import { apiClient } from 'src/shared/lib/apiClient';
import { Story } from 'src/shared/types';

type HomeProps = {
  stories: Story[];
};

export const getServerSideProps: GetServerSideProps<HomeProps> = async () => {
  const response = await apiClient.get<Story[]>('/api/stories');
  return { props: { stories: response.data } };
};

const Home: NextPage<HomeProps> = (props) => {
  const { stories } = props;

  return (
    <div>
      <h1>Home</h1>
      <ul>
        {stories.map((story) => (
          <li key={story.id}>
            {story.id}: {story.title}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Home;

さらに、src/serverディレクトリ以外のディレクトリのファイルもNestJSのビルドファイルに含まれることになるので、nest-cli.jsonを以下のように編集しましょう。

nest-cli.json
{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "entryFile": "server/main"
}

これで、yarn start:devを実行して、 http://localhost:3000/ にアクセスすると、こんな感じの画面が見えるはず

スクリーンショット 2022-02-14 16.33.46.png

これらの情報はすべてサーバーサイドで取得しているので、動的ogpなんかにも使えます。

もし、うまく行かなかった人は、一回yarn buildしてから、再度yarn start:devを実行してみてください。
NestJSのビルドディレクトリdistの中を見て、main.jsがserverディレクトリ内にあるか確認してください。

ここまででcommitしておきます。

動的ルーティングを実装

まずはapp.service.tsに、idを受け取ってstoryを返却する関数を定義します。

src/server/app.service.ts
import { Injectable } from '@nestjs/common';
import { Story } from 'src/shared/types';

const stories: Story[] = [
  {
    id: 1,
    title: '国歌',
    description: '首相が豚と…',
  },
  {
    id: 2,
    title: '1500万メリット',
    description: '超管理社会',
  },
  {
    id: 3,
    title: '人生の軌跡のすべて',
    description: '記憶をデータとして正確に閲覧できる世界の話',
  },
];

@Injectable()
export class AppService {
  getStories() {
    return stories;
  }

  getStory(id: number) {
    return stories.find((story) => story.id === id) ?? null;
  }
}

次に、インターセプターというやつを実装します。rxjs使ってややこしい感じですが、簡単に言うとこれでNestJSのcontrollerからNext.jsのgetServerSidePropsにパスパラメーターを渡してるってことだと思います。

インターセプターのドキュメント(日本語訳): https://zenn.dev/kisihara_c/books/nest-officialdoc-jp/viewer/overview-interceptors

src/server/params.interceptor.tsを以下のように作成します。

src/server/params.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class ParamsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const request = context.switchToHttp().getRequest() as Request;

    return next.handle().pipe(
      map((data) => {
        return {
          ...request.query,
          ...request.params,
          ...data,
        };
      }),
    );
  }
}

そして、app.controller.tsのNext.jsをレンダリングする関数に、UseInterceptorsデコレーターをつけてあげます。

src/server/app.controller.ts
import {
  Controller,
  Get,
  Param,
  Render,
  UseInterceptors,
} from '@nestjs/common';
import { AppService } from './app.service';
import { ParamsInterceptor } from './params.interceptor';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @Render('index')
  @UseInterceptors(ParamsInterceptor)
  home() {
    return {};
  }

  @Get(':id')
  @Render('[id]')
  @UseInterceptors(ParamsInterceptor)
  public storyPage() {
    return {};
  }

  @Get('/api/stories')
  public getStories() {
    return this.appService.getStories();
  }

  @Get('/api/story/:id')
  public getStory(@Param('id') id: number) {
    return this.appService.getStory(Number(id));
  }
}

storyPage()関数で、 http://localhost:3000/:id にアクセスされた時にsrc/pages/[id]/index.tsxをレンダリングします。
getStory関数で、パスパラメータからidを受け取ってストーリーを返します。

仕上げにフロントエンド側を作成しましょう。
src/pages/[id]/index.tsxを以下のように作成します。

src/pages/[id]/index.tsx
import { GetServerSideProps, GetServerSidePropsContext, NextPage } from 'next';
import Link from 'next/link';
import { apiClient } from 'src/shared/lib/apiClient';
import { Story } from 'src/shared/types';

type StoryPageProps = {
  story: Story;
};

export const getServerSideProps: GetServerSideProps<StoryPageProps> = async (
  ctx: GetServerSidePropsContext,
) => {
  const id = ctx.query.id;
  if (id) {
    const response = await apiClient.get<Story | null>(`/api/story/${id}`);
    return response.data
      ? {
          props: { story: response.data },
        }
      : { notFound: true };
  } else {
    return {
      notFound: true,
    };
  }
};

const StoryPage: NextPage<StoryPageProps> = (props) => {
  const { story } = props;

  return (
    <div>
      <Link href={'/'}>
        <a>Home</a>
      </Link>
      <h1>story: {story.title}</h1>
      <p>あらすじ: {story.description}</p>
    </div>
  );
};

export default StoryPage;

最後に、src/pages/index.tsxからのリンクも用意しましょう。

src/pages/index.tsx
import type { GetServerSideProps, NextPage } from 'next';
import Link from 'next/link';
import { apiClient } from 'src/shared/lib/apiClient';
import { Story } from 'src/shared/types';

type HomeProps = {
  stories: Story[];
};

export const getServerSideProps: GetServerSideProps<HomeProps> = async () => {
  const response = await apiClient.get<Story[]>('/api/stories');
  return { props: { stories: response.data } };
};

const Home: NextPage<HomeProps> = (props) => {
  const { stories } = props;

  return (
    <div>
      <h1>Home</h1>
      <ul>
        {stories.map((story) => (
          <li key={story.id}>
            <Link href={`/${story.id}`}>
              <a>
                {story.id}: {story.title}
              </a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Home;

これで、yarn start:devして、http://localhost:3000/ にアクセスすると、リンクが表示されます。(ダメだったら先にyarn buildしてね)

スクリーンショット 2022-02-14 17.50.51.png

また、リンクをクリックしたり、http://localhost:3000/1 にアクセスすると、APIを叩いて以下のようにタイトルとあらすじが表示されます。

スクリーンショット 2022-02-14 21.55.59.png

これで実装完了です。
あとはSSRでサーバーから好きにデータ受け取っちゃいましょう!

ここまでのcommitはこちら

デプロイ

では、いよいよデプロイしていきましょう

herokuのアカウントを作ってログインしておいてください。
メアドがあれば無料でアカウント作成できるし、クレカ情報を登録すれば無料でLinux コンテナを990時間とか起動できるので、完全無料で1個のサービスを常駐させれます。

ログインしたら、create new appを押すか、以下にアクセスしてください。

App nameに好きな名前を入力してください。筆者はnest-next-template-in-herokuにしました。

スクリーンショット 2022-02-14 18.12.59.png

入力できたら、Create appボタンを押してください。

これでデプロイ先のLinux コンテナが起動しました。

では、ここにGitHub Actionsでデプロイします。

.github/workflows/deploy.ymlを以下のように作成します。

.github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: akhileshns/heroku-deploy@v3.12.12
        with:
          heroku_api_key: ${{secrets.HEROKU_API_KEY}}
          heroku_app_name: ${{secrets.HEROKU_APP}}
          heroku_email: ${{secrets.HEROKU_EMAIL}}

これで、masterブランチにpushするとデプロイが走りますが、pushするのはちょっと待ってくださいね。

pushする先のGitHubリポジトリの設定を変更します。(まだ作成していない人は作成しておいてください)

Settings -> Secrets -> Actions から 「New repository secret」ボタンを押してください。
スクリーンショット 2022-02-14 18.23.13.png

NameにHEROKU_API_KEY、ValueにherokuのAPIキーを入力して「Add secret」ボタンを押して保存してください。

herokuのAPIキーはAccount Settingsから、API Key
のところの「Reveal」ボタンを押して表示されたAPIキーをコピペしてあげてください。

スクリーンショット 2022-02-14 18.27.55.png

同様に、「New repository secret」ボタンから、HEROKU_APP, HEROKU_EMAILを設定しましょう。

  • HEROKU_APP: herokuのapp name(筆者の場合はnest-next-template-in-heroku)
  • HEROKU_EMAIL: herokuに登録したメールアドレス

次に、baseURLを環境によって切り替えます。
herokuでApp name一覧からApp nameを選択してSettingsからConfig Varsに、KEY: NODE_ENV, VALUE: productionを設定して、Addボタンを押してください。
これで、デプロイされた時に環境変数process.env.NODE_ENVで参照することができます。

スクリーンショット 2022-02-14 18.36.39.png

これに伴い、shared/lib/apiClient.tsを以下のように編集します。
DEPLOY_URL ってところは、デプロイ先のURLを入力してください。本来は.envとかで定義すべきですが、テンプレートでやるの面倒なので今回はやりません。
ちなみに公開してるリポジトリでは最終的に環境変数も導入してるので、よかったら見てね。

shared/lib/apiClient.ts
import axios from 'axios';

const nodeEnv = process.env.NODE_ENV ?? 'development';

export const apiClient = axios.create({
  baseURL:
    nodeEnv === 'production'
      ? DEPLOY_URL
      : 'http://localhost:3000/',
  responseType: 'json',
  headers: {
    'Content-Type': 'application/json',
  },
});

また、src/server/app.module.tsのdevオプションを変更します。

src/server/app.module.ts
import { Module } from '@nestjs/common';
import { RenderModule } from 'nest-next';
import Next from 'next';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    RenderModule.forRootAsync(Next({ dev: false }), { viewsDir: null }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

これによりNext.jsのdevモードが解除されます。
このオプションに関しても、本来なら環境変数を参照して、開発環境とでデプロイ先で切り替えるような実装が望ましいと思います。

最後にルート直下にProcfileを追加しましょう。これで起動コマンドを指定できます。
ビルドはデプロイ後に自動で走るので、起動コマンドのyarn start:prodを指定してあげます。

Procfile
web: yarn start:prod

これらを設定できたら、コミットしてmasterブランチにプッシュしましょう。

すると、GitHub Actionsが走って…

デプロイ先のURL(筆者の場合は https://nest-next-template-in-heroku.herokuapp.com/ )にアクセスすると、実装したHome画面が表示されると思います。

デプロイのcommitはこちら

まとめ

お疲れ様でした!
めちゃくちゃ丁寧にNext.js+NestJSのモノレポを作成する手順を解説してみました。
これ読んでwebアプリ作ったことないような人もトライしてくれるとすごい嬉しいです!

あと、Twitterもやってますが、特にありがたみのないツイートしかしてないです…
お酒が好きなので、酒飲みは良かったらフォローして友達になってください

31
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?