Zenn
📖

Next.js (App Router) で超絶シンプルな書籍管理アプリを作ってみた

2025/03/03に公開

はじめに

こんにちは!まだまだ見習いエンジニアの🦆です。

会社で「本棚にどんな本があるかわからない」という課題があり、それを解決するためのサービスを初学者なりに試行錯誤しながら作ってみました!🚀

このブログでは、その開発の流れを振り返っていきます。具体的な実装方法には触れませんが、ディレクトリ構成や大まかな進め方を紹介します。

実装の詳細が気になる方は、公式ドキュメントや他の記事をぜひ参考にしてみてください!

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コンポーネントの作成

    こちらは既にUIコンポーネントを共通化したものが社内で使われているため、そちらを使用しました。
    その他候補:Material-UIChakra UI ...

  • ページの作成

    UIコンポーネントを組み合わせて、ページを作成していきました。
    同時に共通化できる箇所(入力フォームやダイアログの開閉切り替えなど)のコンポーネント分けやhook化も行いました。
    ※状態管理にはuseStateuseReducerjotai/atomを使用

  • スタイリング

    スタイルの適用は下記3つを使用しました。
    vanilla-extract/css
     →型安全な CSS を記述でき、スタイルの管理がしやすい
    polished
     →CSS 関数(border, rgba など)を簡潔に記述でき、スタイルの調整が柔軟にできる
    tailwindcss/colors
     →カラーパレットが豊富で、統一感のあるデザインを手軽に適用できる

  • データの取得と管理

    データの取得と管理には標準のFetch APIをベースにしたGraphQLクライアントを実装し、GraphQLAPIと通信してデータの取得・登録・更新を行い、コンポーネントに反映しました。

また、App Routerの規約に即したデータフェッチやディレクトリ構成等はNext.jsの考え方を参考に実装しました。

5.バックエンド開発

ExpressGraphQLを使って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

  • DB設計

    まずは要件を基にDBの設計を行い、マイグレーションを実行してDBを構築しました。
    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());
    
    
  • GraphQLの実装

    GraphQLのスキーマを定義し、QueryMutationResolverを実装しました。
    これでフロントエンドから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.今後の展望

今回の開発では、ひとまず本の管理ができるベース部分を作りましたが、まだまだ改善したい点があります!

  • 認証機能を追加し、誰が本を借りているかを可視化したい

    → 社内の本棚で「この本、今誰が持ってる?」 という状況をすぐに確認できるようにしたい。
  • 書籍登録をバーコードで簡単にできるようにしたい

    ISBNをスキャンするだけで情報が自動入力されると、登録の手間がぐっと減りそう!
  • 本の検索機能の強化

    → タイトルや著者名、ジャンルで簡単に本を検索できるようにし、ユーザーが探している本をスムーズに見つけられるようにしたい。
  • レビュー機能の追加

    → 本を借りた後に感想を残せるようにし、他の人がその本を借りる際の参考になるようなレビュー機能を実装したい。

などなど、、、こういうこと考えてるときってめちゃ楽しいですよね!

8.まとめ

この開発を通して、プロジェクト作成からフロントエンド・バックエンド構築、さらに繋ぎ込みまでの一連の流れを体験できたことは非常に貴重な経験でした。実際に手を動かすことで、理論だけでは得られない実践的な学びを得ることができました。

これまで学んできたTypeScriptフレームワークの知識、バックエンド構築API連携の経験が実際の開発で活かされ、スムーズに進められたことは大きな自信となりました。また、TSConfigcodegenhuskydevcontainerなどの細かいツールの使い方についても理解を深めることができ、とても良かったです。

今後も、今回得た経験を活かしてさらなる成長を目指していきたいと思います!

ユニフォームネクスト株式会社

Discussion

ログインするとコメントできます