Современные способы построения FulІStack-приложений на TypeScript

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Всем привет, я Сергей Пилипенко, JavaScript Developer в DataArt.

В этой статье хотел бы поговорить о современных способах построения Full-stack приложений на TypeScript и рекомендуемые мной технологии. В этой статье я не буду углубляться во все тонкости этого стэка и детали его конфигурации, если в этом будет необходимость, то возможно продолжение.

Зачем

В современном быстроразвивающемся мире технологий одной из основных KPI является TTM (time-to-market). По сути своей этот признак выражает время, необходимое от момента зарождения концепта, до продукта, готового для потребителей. Учитывая это, несложно догадаться, что если вы наймёте отдел разработчиков на условном TypeScript, который сможет закрыть задачи по Back-end, Front-end, Mobile и Desktop, то вы получите либо выигрыш в TTM, либо в деньгах, так как нанять один отдел разработчиков — дешевле четырех.

Почему TypeScript

Большинство людей, читающих эту статью, наверняка знает основные отличия TypeScript от JavaScript. Если же нет, то на просторах интернета есть достаточно ресурсов на эту тему. Но в контексте этого стека разработки появляется ещё одно неочевидное преимущество: типы могут выступать контрактом между Back-end, Front-end и базой данных. Далее об этом — в подразделах Prisma и tRPC.

Core

Ключевыми технологиями в этом стэке являются Prisma ORM, tRPC и фреймворк для SSR/SSG. В качестве такого фреймворка был выбран Next.js.

Prisma

Prisma is an open source next-generation ORM. It consists of the following parts:

Prisma Client: Auto-generated and type-safe query builder for Node.js & TypeScript

Prisma Migrate: Migration system

Prisma Studio: GUI to view and edit data in your database

Каждый проект должен иметь в себе schema файл, который содержит три вещи:

  1. Datasource — отвечает за коннект к БД.
  2. Generator — правила генерации Prisma Client.
  3. Model — собственно сущности в вашей БД.

Пример схемы:

./prisma/schema.prisma

datasource db {
	provider = "postgresql"
	url      = env("DATABASE_URL")
}

generator client {
	provider = "prisma-client-js"
}

model Post {
	id        Int     @id @default(autoincrement())
	title     String
	content   String?
	published Boolean @default(false)
	author    User?   @relation(fields: [authorId], references: [id])
	authorId  Int?
}
model User {
	id    Int     @id @default(autoincrement())
	email String  @unique
	name  String?
	posts Post[]
}

Здесь нас больше всего интересует именно генерация типов в Prisma Client. Prisma автоматически правильно генерирует все типы и зависимости между сущностями на каждое изменение в схеме. Для этого нам нужно запустить команду:

prisma generate

Таким образом наша БД и типы всегда будут синхронизированы. Пример использования сгенерированного клиента:

./prisma.ts

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()
export default prisma;
./someBackend.ts

import prisma from './prisma'

const getAllUsers = async () => {
	const allUsers = await prisma.user.findMany()
  ...
}

tRPC

tRPC — это библиотека, c помощью которой можно задавать список всех необходимых вам энд-поинтов. Основная идея состоит в том, чтобы создать единый router со всеми необходимыми запросами, а потом сделать экспорт его типа и использовать его на клиенте. Пример создания роутера:

./router.ts

import * as trpc from '@trpc/server';
import { z } from 'zod';
import prisma from './prisma';

export const appRouter = trpc
  .router()
  .query("getUser", {
    input: z.object({ name: z.string().min(5) }),
    async resolve({ input: { name } }) {
      const user = await prisma.user.findFirst({ where: { name } });
      return { user };
    },
  })
  .mutation("createUser", {
    input: z.object({ name: z.string().min(5), email: z.string().email() }),
    async resolve({ input: { name, email } }) {
      const user = await prisma.user.create({
        data: {
          name,
          email,
          posts: {
            create: { title: "Initial post" },
          },
        },
      });
      return { user };
    },
  });

export type AppRouter = typeof appRouter;

Здесь мы создаем роутер с двумя операциями getUser и createUser. У каждой из них определены два параметра, путь и resolver. Внутри resolver-a находятся ещё 2 поля:

  1. input — валидатор входящих параметров (в данном примере — это zod).
  2. resolve — имплементация endpoint-a. Также для демонстрации я добавил сюда PrismaClient из примеров выше. И заметьте, ни одного прямого объявления типов, кодогенерация Prisma Client позволяет не думать об этом, но при этом и не жертвовать ими.

Далее перейдем к использованию этого на клиенте.

Клиент

Next.js

Примечание: Использование tRPC с Nuxt

Рассмотрим использование этого на примере Next.js. React на данный момент является наиболее популярной библиотекой для построения интерфейсов.

Долгое время стандартом для новых приложений на React был create-react-app, но обновленная документация рекомендует использовать Next.js для более серьезных проектов.

Пример сетапа с Next.js:

./pages/_app.tsx

import { withTRPC } from '@trpc/next';
import type { AppRouter } from "../../router";
import type { AppProps } from "next/app";

function MyApp({ Component, pageProps }: AppProps) {
  return (
      <Component {...pageProps} />
  );
}

const getBaseUrl = () => {
  if (process.browser) return ""; // Browser should use current path
  if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
  return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
}

export default withTRPC<AppRouter>({
  config({ ctx }) {
    return {
      url: `${getBaseUrl()}/api/trpc`,
    };
  },
  ssr: false, // or true
})(MyApp);

По сути, здесь происходит оборачивание вашего приложения в HOC, который связывает его с tRPC. Здесь необходимо задать url, на котором будет находиться наш trpc-роутер. Далее необходимо объявить API route для tRPC:

./pages/api/trpc/[trpc].ts

import { AppRouter, appRouter } from '../../../router';
import * as trpcNext from '@trpc/server/adapters/next';

export default trpcNext.createNextApiHandler({
  router: appRouter,
  createContext: () => null,
});

Просто используем встроенный в tRPC адаптер для Next.js, с нашим tRPC роутером внутри. И последний шаг настройки — это создание trpc клиента для использования в нашем Next.js приложении.

./utils/trpc.ts
import { createReactQueryHooks } from "@trpc/react";
import type { AppRouter } from "../router";

export const trpc = createReactQueryHooks<AppRouter>();

Этот клиент под капотом использует react-query, так что всё взаимодействие будет выглядеть очень похоже.

Пример использования tRPC на странице:

./pages/index.tsx
import { trpc } from '../utils/trpc'

export default function Home() {
  const {
    data,
    refetch,
    isLoading,
  } = trpc.useQuery(["getUser"]);

  const voteMutation = trpc.useMutation(["createUser"]);
  ...
}

Ни одного прямого объявления типов.

tRPC достаточно умный, чтоб не пропустить несуществующий ключ в useQuery/useMutation и предложит нам autocomplete для всех подходящих вариантов. Так же типизированы и возвращаемые значения.

Пример использования призмы напрямую:

	
export const getStaticProps: GetStaticProps = async () => {
 	const allUsers = await prisma.user.findMany()

  return {
    props: {
      allUsers 
    },
  };
};

Также можно использовать getServerSideProps, но есть свои нюансы.

Что в итоге то

В результате всех этих нехитрых операций, мы получаем беспрерывную интеграцию между БД, клиентом и сервером без необходимости их объявлять напрямую.

Интересные ссылки

Если вас заинтересовала эта тема, то вот несколько интересных ссылок:

  • Next Prisma Starter — Пример репозитория Next.js + Prisma + tRPC от автора tRPC.
  • zART — Next.js + Prisma + tRPC + React Native от автора tRPC.
  • roundest — Какой покемон самый круглый? Более практичный пример репозитория Next.js + Prisma + tRPC и его использования (thanks Theo).
  • tRPC — Документация tRPC.
  • Prisma — Документация Prisma.
  • React Query — Документация React Query.
  • Zod — Run-time валидатор zod.

To be continued...

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось8
До обраногоВ обраному9
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Особливості цих платформ не зникають якщо використовувати спільну мову програмування. Так можна і на Java все писати — GWT для вебу, гола Java для бекенду, Android API-шки для відра, Swing чи SWT/EclipseRCP для десктопу. Але чомусь це так не працює.

Не очень понял суть этого комментария.
Я показал способ взаимодействия при котором мы можем типизировать фронт, бек, бд и мобилку.
Все эти технологии, а именно React Native, React и Node.js, уже и так обширно используются.
При чем тут особенности платформ?

если вы наймёте отдел разработчиков на условном TypeScript, который сможет закрыть задачи по Back-end, Front-end, Mobile и Desktop, то вы получите либо выигрыш в TTM, либо в деньгах, так как нанять один отдел разработчиков — дешевле четырех

Це от про це. Просто реюзати DTO-шки це не половина і навіть не 1/10-а справи.

можем типизировать

Це дуже далеко від одного відділу, який і швець, і жнець, і на десктоп-мобайл кодописець.

Не знаю як кому, а я якось було відкрив документацію Prisma, побачив що вони придумали свій синтаксис моделей, закрив. Не знаю чим їм не підійшов TypeSctipt, чи може там ідея в іншому...

Стосовно фулстека на TypeSctipt, то для цього точно краще підійде Angular (чий код написаний саме на TypeSctipt), ну і на бекенді якийсь NestJS, ну або трохи менш популярний Ditsmod. Спрощення взаємодії між бекендом і фронтендом в рази, у порівнянні із тим, що пропонується в даному пості.

Я же написал «современные способы», а вы тут про Angular :)

Сучасний це як до React/Angular, коли кожен тиждень новий JS фреймворк з’являвся? Ні, дякую, такої «сучасності» не треба — вже проходили.

Если вам действительно интересно, здесь все расписано касательно того почему ментальная модель в призме именно такая и в чём ее отличия от других ORM (Вкратце объекты это не таблицы) www.prisma.io/...​cepts/overview/why-prisma

Касательно Ditsmod, не знаю, нужно будет ознакомится, но всё же я не понимаю каким именно способом вы будете шарить типы между сервером и клиентом в предложенных стэках, если только вы не предлагаете их объявлять руками или выносить в библиотеку.

я не понимаю каким именно способом вы будете шарить типы между сервером и клиентом в предложенных стэках

Ви ж он вище говорили про «современные способы» і не знаєте що таке монорепозиторій?

Перейменовувати змінні краще через F2. Тобто, спочатку стаєте на потрібну змінну, натискаєте F2, потім Enter, і вона автоматично змінюється в усіх місцях.

Это было переименование на клиенте и сервере, там нельзя нажать F2 и автоматом переименовать.

По-перше, це можна легко зробити, я завжди так роблю.

По-друге, ви на GIF показуєте як перейменовуєте змінні на сервері і на клієнті у двох окремих вікнах, і у кожному із вікон робите поштучне перейменування. Тобто, якщо у вас немає розшарених інтерфейсів між сервером і клієнтом, то могли б через F2 перейменувати спочатку на сервері, а потім на клієнті.

По-третє, дивна відповідь від автора поста, який говорить про переваги TypeScript, коли пишеш Fullstack проекти. Щоб розшарити інтерфейси між сервером і клієнтом, потрібно створити монорепозиторій. Ось один із прикладів такого монорепозиторію.

Автор знает про монорепы, автор не хочет везде обьявлять типы напрямую.

Знаєте про монорепи і питаєте таке:

я не понимаю каким именно способом вы будете шарить типы между сервером и клиентом в предложенных стэках

?

TypeScript — костыли над костылями JavaScript (ИМХО).

Так точно, коллега)

Підписатись на коментарі