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

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

まずは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

次に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

パッケージマネージャーを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

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

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: {
...
},
},

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}

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

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;
}

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

フロントにクエリを用意
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>;

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が作成される

storybookの追加
nx add @nx/storybook
nx g @nx/storybook:configuration frontend
起動用コマンド
"sb:start": "nx run frontend:storybook",