Next.js 및 tRPC를 사용한 전체 스택 유형 안전 애플리케이션 개발

목차


  • Introduction
  • tRPC prerequisites
  • tRPC project bootstrap
  • tRPC backend development
  • UI development

  • 소개

    When working with a modern UI library, we often need to fetch data from an external source, a REST or GraphQL API. Synchronizing the client with a server state comes with some challenges, including type definitions and safety. All the external data sources define schemas, but they are not immediately consumable from our UI without redefining them, leveraging types generations libraries, or importing monorepo packages. The lack of immediately available types and the introduction of more dedicated tools create friction and slow developers’ productivity and velocity. Suppose your organization is also split into a front-end and back-end team. The communication overhead adds a layer of potential conflicts because the two parts must agree on the schema before starting the development of features.

    tRPC provides all the tools to create full-stack end-to-end type-safe applications. It is a library that combines the power of TypeScript and react-query to register all the server endpoints and make them instantly available with types to the client. It increases team productivity because backend endpoints are just functions, and the developers can effectively work on the entire stack. Any team can significantly benefit from this tool because code generation, types package, and communication overhead are phased out.

    tRPC also has some downsides. A single developer maintains it. Files cannot be uploaded without any 3rd parties services like Amazon S3. WebSockets have limited support, and data is limited to JSON.

    Pros

    • Full-stack end-to-end type-safe application development.
    • Better team productivity and velocity.
    • Minimal communication overhead.
    • If you trust the type system and there is no complex business logic, unit tests may not be required.

    Cons

    • Solo maintainer.
    • TypeScript could slow down your IDE.
    • Data is limited to JSON.
    • Limited support for WebSocket.

    tRPC 튜토리얼



    tRPC 전제 조건

    The tutorial assumes you are familiar with the following libraries:

    • React,
    • Next.js,
    • Prisma or other ORMs,
    • react-query.

    tRPC 프로젝트 부트스트랩

    Let’s initialize a new tRPC project with the create-t3-app CLI:

    npx create-t3-app@latest

    You will be prompted to answer some questions:

    • Name your application.
    • Select TypeScript.
    • Select Prisma and tRPC.
    • Initialize a git repository.
    • Run npm install .


    프로젝트가 부트스트랩되면 prisma/schema.prisma로 이동하여 새Blog 모델로 업데이트합니다.

    model Blog {
        id        String   @id @default(cuid())
        title     String
        content   String
        createdAt DateTime @default(now())
    }
    


    터미널에서 npx prisma db push를 실행하여 로컬 SQLite 데이터베이스에 모델을 적용합니다.

    tRPC 백엔드 개발

    We are ready to work on the backend of our application. Navigate to src/server/router and create a new file named blog.ts. Please copy the following code to create two endpoints. One handles the creation of a blog post, and the other lists the inserted articles.

    import { createRouter } from "./context";
    // `zod` is a schema declaration and validation library. We use it to define the shape of request arguments.
    import { z } from "zod";
    
    // `createRouter` exposes functions to create new API endpoints.
    export const blogRouter = createRouter()
    // A `mutation` defines an operation that modifies our data.
    // It behaves like`POST`, `PUT`, `DELETE` operations.
    // The input property accepts an optional `zod` schema to validate the request body.
      .mutation("create", {
        input: z.object({
          title: z.string(),
          content: z.string(),
        }),
    // The API handler is a simple `resolve` function that contains all the business logic to handle the request.
        resolve({ input, ctx }) {
          return ctx.prisma.blog.create({
            data: {
              title: input.title,
              content: input.content,
            },
          });
        },
      })
    // A `query` defines an operation that reads data from our backend.
    // It behaves like a GET request.
      .query("all", {
        resolve({ ctx }) {
          return ctx.prisma.blog.findMany();
        },
      });
    

    Once we have defined the routes, we can register them in src/server/router/index.ts.

    // src/server/router/index.ts
    import { createRouter } from "./context";
    import superjson from "superjson";
    
    import { blogRouter } from "./blog";
    
    export const appRouter = createRouter()
      .transformer(superjson)
    // Registering the blogRouter
      .merge("blog.", blogRouter);
    
    // export type definition of the API
    export type AppRouter = typeof appRouter;
    

    UI 개발

    Navigate to src/pages/index.tsx and paste the following code. The tRPC utilities that wrap react-query enable us to access the defined type-safe backend procedures.

    import type { NextPage } from "next";
    import { useState } from "react";
    import BlogPost from "../components/BlogPost";
    import Layout from "../components/Layout";
    
    // import trpc utils that wrap react-query
    import { trpc } from "../utils/trpc";
    
    const Home: NextPage = () => {
    // Hovering on data shows the types defined in our backend
      const { isLoading, data } = trpc.useQuery(["blog.all"]);
    
      return (
        <Layout>
          <h1>My personal blog</h1>
          {isLoading ? (
            <div>Loading posts</div>
          ) : (
            data?.map((blog) => <BlogPost blog={blog} key={blog.id} />)
          )}
          <CreatePostSection />
        </Layout>
      );
    };
    
    export default Home;
    
    const CreatePostSection = () => {
      const [title, setTitle] = useState("");
      const [content, setContent] = useState("");
      const ctx = trpc.useContext();
      const createMutation = trpc.useMutation("blog.create");
    
      return (
        <div>
          {createMutation.isLoading}
          <h2>Create a new blog post</h2>
          <div>
            <input
              name="title"
              placeholder="Your title..."
              value={title}
              onChange={(e) => setTitle(e.target.value)}
            />
          </div>
          <div>
            <textarea
              rows={10}
              cols={50}
              value={content}
              placeholder="Start typing..."
              onChange={(e) => setContent(e.target.value)}
            />
          </div>
          <div>
            <button
              onClick={() =>
    // mutate exposes the types of arguments as defined in our router
                createMutation.mutate(
                  { title, content },
                  {
                    onSuccess() {
                      ctx.invalidateQueries("blog.all");
                      setContent("");
                      setTitle("");
                    },
                  }
                )
              }
              disabled={createMutation.isLoading}
            >
              Create new post
            </button>
          </div>
        </div>
      );
    };
    
    Run npm run dev to check the platform. You can further practice with tRPC by expanding the application with new features. Some examples are finding by id, deleting, or updating a single post. A complete solution can be found here https://github.com/andrew-hu368/t3-blog .

    좋은 웹페이지 즐겨찾기