Next.js (App Router) で超絶シンプルな書籍管理アプリを作ってみた
はじめに
こんにちは!まだまだ見習いエンジニアの🦆です。
会社で「本棚にどんな本があるかわからない」という課題があり、それを解決するためのサービスを初学者なりに試行錯誤しながら作ってみました!🚀
このブログでは、その開発の流れを振り返っていきます。具体的な実装方法には触れませんが、ディレクトリ構成や大まかな進め方を紹介します。
実装の詳細が気になる方は、公式ドキュメントや他の記事をぜひ参考にしてみてください!
1.要件定義
本の管理を簡単に行えるように、以下の基本機能を実装することにしました。
- 書籍の 登録 ・ 一覧表示 を行い、現在の蔵書状況を把握できること
- 書籍ごとの 詳細情報(タイトル・著者など)を閲覧できること
- 誰でも 直感的に操作できるシンプルな UI を備えていること
2.技術選定
学習も兼ねているため、基本的に普段の業務で使用しているモノを選びました。
- フロントエンド: Next.js(App Router)
- バックエンド: Express + GraphQL
- データベース: Prisma + MySQL
3.画面設計
ここはあまり時間使いたくなかったためAIに要件伝えて生成してもらい、
UIマスター(同じチーム内の方)にアドバイス頂きながらFigmaでよりシンプルな画面になるように加工しました。
AIが生成してくれた画面
最終的な画面内容
書籍登録押したとき
編集ボタン押したとき
4.フロントエンド開発
Next.jsを使用し、設計した画面通りになるようにページを作っていきました。
具体的なコード内容には触れませんが、全体のディレクトリ構成と開発の大まかな流れを紹介します。
最終的なディレクトリ構成(主要な部分のみ)
.
├── frontend
│ └── src
│ ├── app
│ │ ├── list
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── components
│ │ └── layouts
│ │ └── PageHeader
│ │ └── index.tsx
│ ├── features
│ │ └── list
│ │ ├── actions
│ │ │ └── book-register-action.ts
│ │ ├── components
│ │ │ ├── BookEditDialog
│ │ │ │ └── presentation.tsx
│ │ │ ├── BookList
│ │ │ │ ├── container.tsx
│ │ │ │ ├── presentation.tsx
│ │ │ │ └── style.css.ts
│ │ │ ├── BookRegisterDialog
│ │ │ │ └── presentation.tsx
│ │ │ └── BookRegisterForm
│ │ │ ├── container.tsx
│ │ │ └── presentation.tsx
│ │ ├── hooks
│ │ │ ├── book-register-form.ts
│ │ │ └── dialog.ts
│ │ ├── constants.ts
│ │ ├── fetcher.ts
│ │ ├── index.ts
│ │ └── types.ts
│ ├── lib
│ │ ├── graphql-sdk.ts
│ │ └── graphql.ts
│ ├── providers
│ │ └── providers
│ │ └── index.tsx
│ └── styles
│ └── global.css.ts
-
こちらは既にUIコンポーネントを共通化したものが社内で使われているため、そちらを使用しました。
その他候補:Material-UI、Chakra UI ... -
UIコンポーネントを組み合わせて、ページを作成していきました。
同時に共通化できる箇所(入力フォームやダイアログの開閉切り替えなど)のコンポーネント分けやhook化も行いました。
※状態管理にはuseState・useReducerやjotai/atomを使用 -
スタイルの適用は下記3つを使用しました。
・vanilla-extract/css
→型安全な CSS を記述でき、スタイルの管理がしやすい
・polished
→CSS 関数(border, rgba など)を簡潔に記述でき、スタイルの調整が柔軟にできる
・tailwindcss/colors
→カラーパレットが豊富で、統一感のあるデザインを手軽に適用できる -
データの取得と管理には標準のFetch APIをベースにしたGraphQLクライアントを実装し、GraphQLのAPIと通信してデータの取得・登録・更新を行い、コンポーネントに反映しました。
また、App Routerの規約に即したデータフェッチやディレクトリ構成等はNext.jsの考え方を参考に実装しました。
5.バックエンド開発
ExpressとGraphQLを使ってAPIを構築していきました。
こちらもフロントエンド同様に具体的なコード内容には触れませんが、全体のディレクトリ構成やschemaの内容と開発の大まかな流れを紹介します。
最終的なディレクトリ構成(主要な部分のみ)
.
├── backend
│ ├── prisma
│ │ ├── migrations
│ │ ├── seeds
│ │ │ └── start.ts
│ │ └── schema.prisma
│ └── src
│ ├── graphqls
│ │ ├── resolvers
│ │ │ ├── book
│ │ │ │ ├── fields.ts
│ │ │ │ ├── mutation.ts
│ │ │ │ └── query.ts
│ │ │ ├── category
│ │ │ │ └── query.ts
│ │ │ ├── location
│ │ │ │ └── query.ts
│ │ │ ├── index.ts
│ │ │ └── type.ts
│ │ ├── context.ts
│ │ ├── dataloader.ts
│ │ ├── model.ts
│ │ ├── schema.ts
│ │ └── type-defs.ts
│ ├── providers
│ │ └── client.ts
│ ├── services
│ │ ├── book.ts
│ │ └── common.ts
│ └── index.ts
-
projects/packages/backend/prisma/schema.prisma
generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model categories { id Int @id name String @unique @db.VarChar(191) books books[] } model locations { id Int @id name String @unique @db.VarChar(191) books books[] } model books { id String @id @default(cuid()) title String @unique @db.VarChar(191) author String @db.VarChar(191) category_id Int category categories @relation(fields: [category_id], references: [id]) location_id Int location locations @relation(fields: [location_id], references: [id]) publication_date DateTime @db.Date }
projects/packages/backend/prisma/seeds/start.ts
import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); const main = async () => { await prisma.categories.createMany({ data: [ { id: 1, name: 'プログラミング', }, { id: 2, name: 'デザイン', }, { id: 3, name: 'マーケティング', }, { id: 4, name: 'ビジネス', }, ], }); await prisma.locations.createMany({ data: [ { id: 1, name: 'システム', }, { id: 2, name: '営業', }, ], }); await prisma.books.createMany({ data: [ { id: 'uuid1', title: 'プログラミング TypeScript', author: 'オライリージャパン', category_id: 1, location_id: 1, publication_date: new Date('2021-01-01'), }, { id: 'uuid2', title: '営業の基本', author: 'ジョン・ブラウン', category_id: 2, location_id: 2, publication_date: new Date('2021-01-01'), }, ], }); }; main() .catch((e) => console.error(e)) .finally(() => prisma.$disconnect());
-
これでフロントエンドからAPIを叩いてデータ操作可能になります!schema.graphql
scalar Date type Book { id: ID! title: String! author: String! category: Category! location: Location! publicationDate: Date! } type Category { id: Int! name: String! } type Location { id: Int! name: String! } input booksInput { searchText: String categoryId: Int locationId: Int } input AddBookInput { title: String! author: String! categoryId: Int! locationId: Int! publicationDate: Date! } input UpdateBookInput { id: ID! title: String! author: String! categoryId: Int! locationId: Int! publicationDate: Date! } type Query { books(input: booksInput = null): [Book!]! categories: [Category!]! locations: [Location!]! } type Mutation { addBook(input: AddBookInput!): Boolean! updateBook(input: UpdateBookInput!): Boolean! }
6.学びと反省
-
検索やフィルターの実装において、どのアプローチが最適か悩むことがありました。具体的には、すべてのデータをフロントエンドで取得してからフィルターする方法と、条件をバックエンドに渡してフィルターしたデータを返す方法のどちらが良いかという点です。
バックエンドでフィルターを行う方が、特にデータ量が多くなると効率的であることが分かりました。また、クエリパラメータを使って検索やフィルター条件を管理することで、他の人にURLを共有する際に、その状態を簡単に再現できるという利点もありました。
-
今回の開発では、普段業務で使っている技術を選びました。これは、単に慣れているからというだけでなく、学習も兼ねて「実際に手を動かして理解を深めたい」という意図もありました。
とはいえ、技術選定の際に「本当にこのフレームワークが最適か?」という視点が足りなかったのも事実です。結果的に、機能要件に対して最適な選択ができていたのかは少し曖昧なまま進めてしまいました。
今後は、いろいろなフレームワークやライブラリに触れ、それぞれのメリット・デメリットを理解した上で技術を選べるようになりたいと思います。
「とりあえずこれでいいか」ではなく、「この技術が一番合っている!」と言えるように、もう少し視野を広げていきたいです!
7.今後の展望
今回の開発では、ひとまず本の管理ができるベース部分を作りましたが、まだまだ改善したい点があります!
などなど、、、こういうこと考えてるときってめちゃ楽しいですよね!
8.まとめ
この開発を通して、プロジェクト作成からフロントエンド・バックエンド構築、さらに繋ぎ込みまでの一連の流れを体験できたことは非常に貴重な経験でした。実際に手を動かすことで、理論だけでは得られない実践的な学びを得ることができました。
これまで学んできたTypeScriptやフレームワークの知識、バックエンド構築やAPI連携の経験が実際の開発で活かされ、スムーズに進められたことは大きな自信となりました。また、TSConfigやcodegen、husky、devcontainerなどの細かいツールの使い方についても理解を深めることができ、とても良かったです。
今後も、今回得た経験を活かしてさらなる成長を目指していきたいと思います!
Discussion