Open13

NxとReactとNestとGraphQLとetcで始める型安全な個人開発モノレポ構成

moppiiiiimoppiiiii

NxとReactとNestとGraphQLとetcで始める型安全な個人開発モノレポ構成を作ります
環境構築のメモ的な立ち位置でのスクラップです

moppiiiiimoppiiiii

まずはmonorepoのworkspaceを作る

npx create-nx-workspace [name]

y

create-nx-workspace@19.2.2
Ok to proceed? (y)

フロントはReact

Which stack do you want to use? … 
None:          Configures a TypeScript/JavaScript project with minimal structure.
React:         Configures a React application with your framework of choice.
Vue:           Configures a Vue application with your framework of choice.
Angular:       Configures a Angular application with modern tooling.
Node:          Configures a Node API application with your framework of choice.

純粋なReactで作成

What framework would you like to use? … 
None          I only want react and react-dom
Next.js       [ https://nextjs.org/      ]
Remix         [ https://remix.run/       ]
Expo          [ https://expo.io/         ]
React Native  [ https://reactnative.dev/ ]

モノレポ構成にするのでIntegrated Monorepoを選択

Integrated monorepo, or standalone project? … 
Integrated Monorepo:  Nx creates a monorepo that contains multiple projects.
Standalone:           Nx creates a single project and makes it fast.

nameはfrontendにする

Application name › [name]

Viteを使いたい

Which bundler would you like to use? … 
Vite    [ https://vitejs.dev/     ]
Webpack [ https://webpack.js.org/ ]
Rspack  [ https://www.rspack.dev/ ]

Playwright一択

Test runner to use for end to end (E2E) tests … 
Playwright [ https://playwright.dev/ ]
Cypress [ https://www.cypress.io/ ]
None

SASSを使いたい

Default stylesheet format … 
CSS
SASS(.scss)       [ https://sass-lang.com   ]
LESS              [ https://lesscss.org     ]
tailwind          [ https://tailwindcss.com     ]
styled-components [ https://styled-components.com            ]
emotion           [ https://emotion.sh                       ]
styled-jsx        [ https://www.npmjs.com/package/styled-jsx ]

nx cloudはスキップする

Do you want Nx Cloud to make your CI fast? …  
(it's free and can be disabled any time)
Yes, enable Nx Cloud
Yes, configure Nx Cloud for GitHub Actions
Yes, configure Nx Cloud for Circle CI
Skip for now

これでapps配下にfontendとfrontend-e2eができていればOK

moppiiiiimoppiiiii

次にNestのバックエンドを作る

nx add @nx/nest

バックエンドのnameはbackendにします

nx g @nx/nest:app backend --frontendProject frontend

apps配下に作るのでDerivedを選択

What should be the project name and where should it be generated? … 
❯ As provided:
    Name: backend
    Root: backend
  Derived:
    Name: backend
    Root: apps/backend

apps配下にbackendとbackend-e2eができていればOK

moppiiiiimoppiiiii

パッケージマネージャーをpnpmに変える

pnpm-lock.yamlを作成する

pnpm i

packege-lock.jsonは必要ないので削除

ローカル起動のスクリプトを用意する
ついでにbuildも用意

package.json

  "scripts": {
    "start": "nx run-many --target serve --all --parallel",
    "build": "nx run-many --target build --all --parallel"
  },
pnpm start

起動確認してOK

moppiiiiimoppiiiii

prettierを調整する

.prettierrc

{
  "printWidth": 120,
  "tabWidth": 2,
  "singleQuote": true,
  "semi": true
}

各アプリケーション用にprettierのcheckとfixコマンド用意しておく

package.json

  "scripts": {
    "start": "nx run-many --target serve --all --parallel",
    "build": "nx run-many --target build --all --parallel",
    "backend:prettier:check": "prettier --check \"apps/backend/**/*.{js,ts,tsx}\"",
    "backend-e2e:prettier:check": "prettier --check \"apps/backend-e2e/**/*.{js,ts,tsx}\"",
    "frontend:prettier:check": "prettier --check \"apps/frontend/**/*.{js,ts,tsx,scss}\"",
    "frontend-e2e:prettier:check": "prettier --check \"apps/frontend-e2e/**/*.{js,ts,tsx}\"",
    "backend:prettier:fix": "prettier --write \"apps/backend/**/*.{js,ts,tsx}\"",
    "backend-e2e:prettier:fix": "prettier --write \"apps/backend-e2e/**/*.{js,ts,tsx}\"",
    "frontend:prettier:fix": "prettier --write \"apps/frontend/**/*.{js,ts,tsx,scss}\"",
    "frontend-e2e:prettier:fix": "prettier --write \"apps/frontend-e2e/**/*.{js,ts,tsx}\""
  },

nx run-manyで一括実行するコマンドをcheckとfixで用意する

package.json

"format:check": "nx run-many --target format:check --all --parallel",
"format:fix": "nx run-many --target format:fix --all --parallel",

上記だけでは動かないので、各アプリケーション側でコマンド定義してあげる
frontendだけ例に挙げて他は省略

apps/frontend/project.json

{
  "name": "frontend",
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "apps/frontend/src",
  "projectType": "application",
  "tags": [],
  "// targets": "to see all targets run: nx show project frontend --web",
  "targets": {
    "format:check": {
      "executor": "nx:run-commands",
      "cache": true,
      "options": {
        "commands": [
          "npm run frontend:prettier:check",
          "npm run frontend:stylelint:check"
        ],
        "parallel": false
      }
    },
    "format:fix": {
      "executor": "nx:run-commands",
      "cache": true,
      "options": {
        "commands": [
          "npm run frontend:prettier:fix",
          "npm run frontend:stylelint:fix"
        ],
        "parallel": false
      }
    }
  }
}

コマンド2回ずつ実行して確認

pnpm format:check
pnpm format:fix

全てのコマンドが一括実行されること、2回目以降nxのキャッシュがヒットすることを確認できればOK

moppiiiiimoppiiiii

ESLintを調整する

ここは好みもあるのでスキップするが、tsconfig.base.jsonに共通系をまとめる場合
各アプリケーション側でパスの解決ができないので注意

const path = require('path');

{
      files: ['*.ts'],
      plugins: ['@typescript-eslint'],
      parser: '@typescript-eslint/parser',
      parserOptions: {
        project: [path.resolve(__dirname, './tsconfig.base.json')],
        ...
      },
      rules: {
        ...
      },
    },
moppiiiiimoppiiiii

lefthook入れる
欠かせない

pnpm i -D lefthook

nxでcacheを効かせているのでglobは無し

lefthook.yaml

pre-commit:
  parallel: false
  commands:
    audit:
      run: pnpm audit
    eslint:fix:
      run: pnpm lint:fix && git add {staged_files}
    prettier:fix:
      run: pnpm format:fix {staged_files} && git add {staged_files}
moppiiiiimoppiiiii

GHAで静的解析追加

on:
  workflow_dispatch:
  push:

jobs:
  static-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v3
        with:
          node-version: 20
      - run: npm install -g pnpm
      - run: pnpm i
      - run: pnpm lint:check
      - run: pnpm format:check
      - run: pnpm build
moppiiiiimoppiiiii

graphQL関連の導入

pnpm i @apollo/client @apollo/server @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo @nestjs/apollo @nestjs/graphql graphql

frontend

main.tsx

import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';

import { ApolloClient, InMemoryCache, ApolloProvider, HttpLink } from '@apollo/client';

import App from './app/app';

const httpLink = new HttpLink({
  uri: '/graphql'
});

const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache()
});

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </StrictMode>
);

backend
後ほどgraphqlのcode genを実行するのでresolverも作っておく

app.module.ts

import { Module } from '@nestjs/common';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { UserResolver } from './resolvers/user/user.resolver';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService, UserResolver],
})
export class AppModule {}
user.resolver.ts

import { Resolver, Query } from '@nestjs/graphql';
import { User } from '../../models/user/user.model';

const userSampleList: User[] = [
  {
    id: 'uuid1',
    email: 'sample1@example.co.jp',
    name: 'sample1',
  },
  {
    id: 'uuid2',
    email: 'sample2@example.co.jp',
    name: 'sample2',
  },
  {
    id: 'uuid3',
    email: 'sample3@example.co.jp',
    name: 'sample3',
  },
];

@Resolver('User')
export class UserResolver {
  @Query(() => [User])
  async users(): Promise<User[]> {
    return userSampleList;
  }
}
user.model.ts

import { Field, ID, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => ID)
  id: string;

  @Field()
  email: string;

  @Field()
  name: string;
}

moppiiiiimoppiiiii

graphql codegenの整備

"gql:gen": "graphql-codegen --config codegen.yaml",
codegen.yaml

overwrite: true
schema: http://localhost:3000/graphql
documents: './apps/frontend/src/app/graphql/queries/**/*.tsx'
generates:
  ./apps/frontend/src/app/graphql/generate/generated.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      fetcher: fetch
moppiiiiimoppiiiii

フロントにクエリを用意

import { gql } from '@apollo/client';

export const USER = gql`
  query GetUsers {
    users {
      id
      email
      name
    }
  }
`;
pnpm gql:gen

graphqlフォルダ配下にクエリhookが作成されていればOK

import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
const defaultOptions = {} as const;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: { input: string; output: string; }
  String: { input: string; output: string; }
  Boolean: { input: boolean; output: boolean; }
  Int: { input: number; output: number; }
  Float: { input: number; output: number; }
};

export type Query = {
  __typename?: 'Query';
  users: Array<User>;
};

export type User = {
  __typename?: 'User';
  email: Scalars['String']['output'];
  id: Scalars['ID']['output'];
  name: Scalars['String']['output'];
};

export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>;


export type GetUsersQuery = { __typename?: 'Query', users: Array<{ __typename?: 'User', id: string, email: string, name: string }> };


export const GetUsersDocument = gql`
    query GetUsers {
  users {
    id
    email
    name
  }
}
    `;

/**
 * __useGetUsersQuery__
 *
 * To run a query within a React component, call `useGetUsersQuery` and pass it any options that fit your needs.
 * When your component renders, `useGetUsersQuery` returns an object from Apollo Client that contains loading, error, and data properties
 * you can use to render your UI.
 *
 * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
 *
 * @example
 * const { data, loading, error } = useGetUsersQuery({
 *   variables: {
 *   },
 * });
 */
export function useGetUsersQuery(baseOptions?: Apollo.QueryHookOptions<GetUsersQuery, GetUsersQueryVariables>) {
        const options = {...defaultOptions, ...baseOptions}
        return Apollo.useQuery<GetUsersQuery, GetUsersQueryVariables>(GetUsersDocument, options);
      }
export function useGetUsersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetUsersQuery, GetUsersQueryVariables>) {
          const options = {...defaultOptions, ...baseOptions}
          return Apollo.useLazyQuery<GetUsersQuery, GetUsersQueryVariables>(GetUsersDocument, options);
        }
export function useGetUsersSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<GetUsersQuery, GetUsersQueryVariables>) {
          const options = {...defaultOptions, ...baseOptions}
          return Apollo.useSuspenseQuery<GetUsersQuery, GetUsersQueryVariables>(GetUsersDocument, options);
        }
export type GetUsersQueryHookResult = ReturnType<typeof useGetUsersQuery>;
export type GetUsersLazyQueryHookResult = ReturnType<typeof useGetUsersLazyQuery>;
export type GetUsersSuspenseQueryHookResult = ReturnType<typeof useGetUsersSuspenseQuery>;
export type GetUsersQueryResult = Apollo.QueryResult<GetUsersQuery, GetUsersQueryVariables>;
moppiiiiimoppiiiii

module.scssの型定義を生成できるようにする

pnpm i vite-plugin-sass-dts
vite.config.ts

  plugins: [
    react(),
    nxViteTsPaths(),
    sassDts({
      enabledMode: ['development', 'production'],
      global: {
        generate: false,
        outputFilePath: '',
      },
    }),
  ],

これでscssファイルを保存した際、同階層にmodule.scss.d.tsが作成される

moppiiiiimoppiiiii

storybookの追加

nx add @nx/storybook
nx g @nx/storybook:configuration frontend

起動用コマンド

"sb:start": "nx run frontend:storybook",