Галоўная / Артыкулы

MovieVault — гід: API для стварэння спіса абяцоў з Express 5, Prisma 7 і JWT

MovieVault – гэта заведама практыка для выконання на всіх роўнях: API для списку для старання на базе Express 5, Prisma 7 і JWT, з прыміткамі па адгляду ў чынніце пераканання ў правах на власнасць, каскадных адпаведнасцях і обробцы памылак.

Выдавец: REACTAPP.TOP14 хв чытання3198 слоў

Крыніцы: PRISMA SCHEMA SIMPLE TOUCH (BACKEND)

Вправы з жывага кодавання з часамі є распашным спосабом адцэнкавання разработчыкаў full-stack, і яны награджаюць кандыдатаў, якія можуць быстра ператворыць короткія спефікацыі на працюючы, захіщаны API. У гэтым кераванні выкорыстоўваецца адна такая короткая спефікацыя — каталог фільмаў пад назвай MovieVault з персональным спискам для аблікування фільмаў, і яна поўнаважваецца цэлым бэкендам на базе Express, TypeScript і Prisma. Вы пазнаёсце, як модэль дадзеных выражае адносы «многа-на-многа», як правілы аутантыкацыі і власнасці пераадражваюцца ў проверкách мідлвэра і кантролера, а таксама якія недагэчанасці ў рашынце з часовым лімітам варта вылечыць прычымо да запуску ў працэсе.

Спефікацыя MovieVault

Програма дазволяе корыстувачам пераглядаць каталог фільмаў, зберагаць ўснавыя назвы у персональны списак для аблікування і кераваць якімі-то з іх. Неабходныя функцыі ўключаюць:

  • Рэгістрацыя і заходжанне.
  • Спісак доступных фільмаў, з можлымасцю выдалення фільма з галоўнага каталогу.
  • Дадаванне фільма да сашытнага списку корыстувальця.
  • Рэдагаванне атрыбутаў адраска та рэйтынгу элемента сашытнага списку. Гэта прымусова кольяборатыўны процес: будзь-хто з усёйлогаваных корыстувальнікаў можа рэдагаваць будзь-які элемент, а не толькі той, хто яго стварыў.
  • Выдаленне фільма з сашытнага списку, што можа выконваць толькі власнік гэтага элемента.
  • Сервер выкарыстоўвае Express, TypeScript і Prisma; кліент — React, Tailwind і ShadCN UI.

    Асновныя правілы

    • Часовы ліміт станавіць 280 хвілін.
    • Работа павінна быць вашай сопрацоўнічай працой.
    • Катэгорыю node_modules павінна быць выключана за дапамогою файла .gitignore, а файл .env.example павінен быць задаўлены.
    • Для такіх основных компанентаў, як Button, Card і Input, выкарыстоўванне ShadCN UI є обав’язковым.
    • Пакты трэба адрабатваць на сервере, а ўжо пасля — падаць іх корыстніку на кліентскай стороне, прынеймна за дапамогою console.log або паведамлення.

    Настройка сервера і модэлю дадзеных

    Сервер — это проект на Express, напісаны ў TypeScript з Prisma як ORM. У завданні запрашваецца стварыць тры моделі: User (поле id, якое можа быць цэлым числам або строкай, унікальны email, password і name), Movie (id, title, genre, description, imageUrl) і модель списку для адзірвання фільмаў (id, userId, movieId, строка review, якая за замовчаннем ўжо пустая, і цэлае число rating, якае за замовчаннем доравнае 1)). Карыстальнікі і фільмы асоціююцца межаўсяе-межаўсяе через табелю списку для адзірвання, і прынеймна пяць фільмаў должны быць заполненыя з рукапіснага файла JSON у пададзе server/data/movies.json.

    Наведзены ў нижчым фрагменте схема рэалізуе гэта за дапамою явнага модэлю з’яеднання. Два аспекты заслуговуюць на увагу. Абмежэнне @@unique([userId, movieId]) означае, што корыстнік не можа зберагчы той самы фільм два разы, а таксама дае Prisma складны ключ пад назвай userId_movieId, які пазней викорыстоўваюць кантролеры для пошуку. Блок datasource не мае атрыбута url: у Prisma 7 стрэлка з’яеднання насталявалася за межамі схемы (у файле prisma.config.ts) і кліент падключаецца через адаптар драйвера, таму ў спісе залежнасцэў прыказаны @prisma/adapter-pg і pg. Па ўсімі крокам насталоўкі адзірніце насталоўку Prisma 7 з PostgreSQL у проекте на TypeScript Node.js.

    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "postgresql"
    }
    
    model User {
      id Int @id @default(autoincrement())
      email String @unique
      name String
      password String
      watchList WatchList[]
    }
    
    model Movie {
      id Int @id @default(autoincrement())
      title String
      genre String
      description String
      imageUrl String
      watchList WatchList[]
    }
    
    model WatchList{
      id Int @id @default(autoincrement())
      review String @default("")
      rating Int @default(1)
      userId Int
      movieId Int
      user User @relation(fields: [userId], references: [id] )
      movie Movie @relation(fields: [movieId], references: [id] )
      @@unique([userId, movieId])
    }
    

    Насталоўка кліента

    Фронтэнд — это прыжак для даплікаў на React, створаны з Vite, адзначаны Tailwind CSS і ShadCN UI, а маршрутызацыя выканана за дапамою React Router. У спефікацыі кожны серверскі этап супараднаваны з кліентскай адпаведнаю экранам:

    • Старонкі /login і /register створаны з компонентаў формы з ShadCN. Пасля успешнага заходжання користувача токен зберагаецца, а ён пераправляецца на панель керування за адресай /.
    • Панель керування, якая адображае фільмы у вачэйцы з картаклів ShadCN. Кожная картка мае кнопкі „Дадзіць у список для перагляду“ і „Адмахнуць фільм“, пры чым адмахненне павінна негаразу апдэйтаваць вачэйку.
    • Старонка /my-watchlist, якая паказвае фільмы, зберажаныя корыстувачам, а таксама review і rating кожнага з іх.
    • Процес рэдагавання элементаў списку для перагляду, будзь то чераз дыялогавое вікно ShadCN або окремую старонку, пры чым інтэрфейс апдэйтуецца негаразу пасля успешнага змены.

    Контракт API

    • POST /register — рэгіструе ўжоўка; атрыбуты апраўнення не выкананы.
    • POST /login — вяртае токен доступу JWT; атрыбуты апраўнення не выкананы.
    • GET /movies — паказвае всі фільмы; апраўнення неабходна.
    • DELETE /movies/:id — адчыняе фільм з каталогу; апраўнення неабходна.
    • GET /watchlist — паказвае элементы чатэнні перадварота нынешняга ўжоўка, выкорыстоўваючы include прыметніка Prisma для дадаўання дакладнасцей пра фільмы; апраўнення неабходна.
    • POST /watchlist/:movieId — дадае фільм у чатэнню перадварота нынешняга ўжоўка; апраўнення неабходна.
    • PATCH /watchlist/:id — аднавляе атказ і рэйтынг без пераканання ў правах власніка; апраўнення неабходна.
    • DELETE /watchlist/:id — адчыняе элемент чатэнні; апраўнення неабходна і даступ развярнуты толькі власніку.

    Паролі павінны быць хэшаваныя за дапамогою bcrypt.

    Рэгістрацыя і заход

    Контралер аутэнтыкакціі карае як публічныя, так і іншыя канцэнтры. Функцыя register адхоўчае запит з кодам 400, калі які-небудзь поль не заполнены, гэшуе пароль за дапамою bcrypt з салтам касцы 12, стварае пользователя і негайна падписвае JWT, які містыць id, name і email пользователя, пры чым тэрмін дзейнасці такога JWT складае адны дзень. Функцыя login шукае пользователя па адресе электроннай пошты, порыхуе пароль за дапамою bcrypt.compare і вяртае той самы тип токена. Обе функцыі вяртаюць толькі безпечныя поля пользователя, ніколі не вяртаючы гэша.

    import { Request, Response } from "express";
    import prisma from "../libs/prisma";
    import bcrypt from "bcrypt";
    import jwt from "jsonwebtoken";
    
    export const register = async (req: Request, res: Response) => {
      try {
        const { name, email, password } = req.body;
        if (!name || !email || !password) {
          return res.status(400).json({
            message: "Semua kolom harus diisi",
          });
        }
        const salt = await bcrypt.genSalt(12);
    
        const hashedPassword = await bcrypt.hash(password, salt);
        const newUser = await prisma.user.create({
          data: {
            name: name,
            email: email,
            password: hashedPassword,
          },
        });
    
        const token = jwt.sign(
          {
            id: newUser.id,
            name: newUser.name,
            email: newUser.email,
          },
          process.env.JWT_SECRET as string,
          { expiresIn: "1d" },
        );
    
        return res.status(201).json({
          message: "User Berhasil di buat",
          token: token,
          data: {
            id: newUser.id,
            name: newUser.name,
            email: newUser.email,
          },
        });
      } catch (error) {
        return res.status(500).json({
          message: "gagal membuat user",
          error: error,
        });
      }
    };
    
    export const login = async (req: Request, res: Response) => {
      try {
        const { email, password } = req.body;
        const user = await prisma.user.findUnique({
          where: { email: email },
        });
    
        if (!user) {
          return res.status(400).json({
            message: "Email/Username/Password salah",
          });
        }
    
        const isPasswordValid = user
          ? await bcrypt.compare(password, user.password)
          : false;
    
        if (!isPasswordValid) {
          return res.status(400).json({
            message: "Email/Username/Password salah",
          });
        }
    
        const token = jwt.sign(
          {
            id: user.id,
            name: user.name,
            email: user.email,
          },
          process.env.JWT_SECRET as string,
          { expiresIn: "1d" },
        );
    
        return res.status(200).json({
          message: "Login Berhasil",
          token: token,
          data: {
            id: user.id,
            email: user.email,
            name: user.name,
          },
        });
      } catch (error) {
        return res.status(500).json({
          message: "Terjadi kesalahan pada server",
          error: error,
        });
      }
    };
    

    Тое, што варта пасілкнуць:

    • Пры заходзе на адмінку для невядомай адресы электроннай пошты чы правильнага пароля викорыстоўваецца адны і той жа код памилкі, што не дазволяе выявіць, якія акаунты існуюць. Вярненне коду 401 замест 400 дазволіла бы болей тачна описаць памилку.
  • Рэгістрацыя адраса электранайтов, які ўжо існуе, спрычыная працэю правіла унікальнасці, што заканчываеся стандартным кодам 500. Адхаптаванне падаючай памылкі працэўніка Prisma аб наявнасці правалення унікальнасці і адпаведная вяртанне коду 409 дае кліенту можлівасць прыняць практычныя заходы.
  • Оба блакі перехвату адправляюць неапдэтаваны об’ект error кліенту. Яго трэба зафіксаваць на сервере і вярнуць толькі паведамленне.
  • process.env.JWT_SECRET як строка глушыць компілятор, але не час выканання; якщо гэтая зменная відсутня, падписванне неякшае на кожны запит. Неабходнае перакананне значэнняя сяродовых зменных трэба адрабатаваць пад час запуску.
  • Для адаптаванага варыянта гэтага процесу адзірвання, пазірьце аутентыфікацыю JWT, гатовую да выкорыстання ў API на Node.js.

    Мідлвэр аутентыфікацыі

    Мідлвэр чытае загалоўку Authorization, берае частку пасля першага прабела як токен і адпавядае кодам 401, калі яго няма. Пасля чаго ён пераканаецца ў правамі токена за дапамою спяльнай таёмнай канфігурацыі і прыўяжае раскодаваны пакет да запиту, каб контролеры моглі прачытаць id пользователя. Некоректны або выйшлы з часу токен таксама прыводзіць да коду 401.

    import { Request, Response, NextFunction } from "express";
    import jwt from "jsonwebtoken";
    
    export const authentication = (
      req: Request,
      res: Response,
      next: NextFunction,
    ) => {
      const authHeader = req.headers.authorization;
      const token = authHeader && authHeader.split(" ")[1];
    
      if (!token) {
        return res.status(401).json({ message: "No token provided" });
      }
    
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET as string);
        (req as any).user = decoded;
        next();
      } catch (error) {
        return res.status(401).json({ message: "Invalid token" });
      }
    };
    

    Пераклад (req as any).user работае, але ён усуняе перакананне па типу ў кожным контролеры, які яго чытае. Расшырэнне інтарфейсу Request у Express за дапамою злучэння декларацый падтрымвае типізаванне req.user. Аналіз загалоўак таксама прыманяе любую схему, а не толькі Bearer.

    Залежнасці проекта

    package.json фіксуе версіі компанентаў у момент стварэння: Express 5, Prisma 7 з адаптэрам для PostgreSQL, bcrypt, jsonwebtoken і cors, а таксама TypeScript і ts-node-dev для скрыпта dev, які перзапускаеся пасля змян. Дыапазоны версій быстра зменшваюцца, таму пераканайцеся ў актуальных выданнях пры ўторым викорыстоўванні.

    {
      "name": "server",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "dev": "ts-node-dev --respawn --transpile-only src/index.ts",
        "build": "tsc"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "type": "commonjs",
      "dependencies": {
        "@prisma/adapter-pg": "^7.8.0",
        "@prisma/client": "^7.8.0",
        "bcrypt": "^6.0.0",
        "cors": "^2.8.6",
        "express": "^5.2.1",
        "jsonwebtoken": "^9.0.3",
        "pg": "^8.22.0"
      },
      "devDependencies": {
        "@types/bcrypt": "^6.0.0",
        "@types/cors": "^2.8.19",
        "@types/express": "^5.0.6",
        "@types/jsonwebtoken": "^9.0.10",
        "@types/node": "^26.0.1",
        "prisma": "^7.8.0",
        "ts-node-dev": "^2.0.0",
        "typescript": "^6.0.3"
      }
    }
    

    Заазначыце, што нічога ў гэтым списку не завантажвае файл .env у process.env, таму вам патрэбны такія інструменты як dotenv або падтрымка файлаў налашчэння вашай среды для таго, каб JWT_SECRET быў доступны.

    Канцэнтры для фільмаў

    getMovies простаўляе ўсі фільмы. deleteMovie аналізуе параметр маршруту: якщо ID не ў формате числа, вона вяртае код 400, а якщо ID невядомы — код 404; пасля чаго запускае $transaction, які спачатку вычышае всі рэкорды справакі пра перагляд, якія асоціююцца з фільмам, а потым сам фільм. Вычышэнне дзецячых рэкордаў спачатку запобегае наявнасці правалянняя зовнішньага ключа, а транзакцыя гарантуе, што оба кроку будуць або успехамі, або няўспэхамі разам.

    import { Request, Response } from "express";
    import prisma from "../libs/prisma";
    
    // GET /movies - Mendapatkan semua daftar film
    export const getMovies = async (req: Request, res: Response) => {
      try {
        const movies = await prisma.movie.findMany();
        return res.status(200).json(movies);
      } catch (error) {
        console.error("Get Movies Error:", error);
        return res.status(500).json({
          message: "Terjadi kesalahan pada server saat mengambil data film",
        });
      }
    };
    
    // DELETE /movies/:id - Menghapus film dari katalog utama
    export const deleteMovie = async (req: Request, res: Response) => {
      try {
        const { id } = req.params;
        const movieId = parseInt(id, 10);
    
        if (isNaN(movieId)) {
          return res.status(400).json({ message: "ID film tidak valid" });
        }
    
        // Periksa apakah film ada
        const movie = await prisma.movie.findUnique({
          where: { id: movieId },
        });
    
        if (!movie) {
          return res.status(404).json({ message: "Film tidak ditemukan" });
        }
    
        // Menggunakan Transaction untuk menghapus watchlist terkait terlebih dahulu
        // guna menghindari pelanggaran batasan foreign key (kunci asing)
        await prisma.$transaction([
          prisma.watchList.deleteMany({
            where: { movieId: movieId },
          }),
          prisma.movie.delete({
            where: { id: movieId },
          }),
        ]);
    
        return res.status(200).json({
          message: "Film berhasil dihapus dari katalog utama",
        });
      } catch (error) {
        console.error("Delete Movie Error:", error);
        return res.status(500).json({
          message: "Terjadi kesalahan pada server saat menghapus film",
        });
      }
    };
    

    Альтернатыва — задаць onDelete: Cascade на асоціяцыі movie справакі пра перагляд, чым дазволіць базе дадзеных аўтаматычна вычысіць залежныя рэкорды. Актыўная транзакцыя ёсць болей відчутнаю ў коде; каскад гораздка легва забыць.

    Канцэнтры справакі пра перагляд

    Контролер справакі пра перагляд мае найцякавейшыя правілы:

    • getWatchlist фільтруе за ID пользователя з токена і выкарыстоўвае include: { movie: true }, тады кожны элемент прыходзіць разам з адпаведным фільмам.
    • addToWatchlist пераканальвае ID фільма, паўтарная пераканальвае, што фільм існуе, шукае кампаундны ключ userId_movieId, ўбачваючы дуплікаты і вяртаючы код 400, а таксама стварае элемент з стандартнымі атрыбутамі адгукнення і рэйтынгу.
    • updateWatchlist рэалізуе правіла саавершання: яна пераканальвае, што элемент існуе, але не выявляе, хто яго володзіць. Якщо ўказаны rating, яго неабходна пераканаліць у цэлычысленную значенне з дыяпазону 1 да 5. Поле, якое залишаецца undefined, ігнаруецца Prisma, таму частковая адчынка паўтарная тыкайцца толькі таго, што было адправлена.
    • deleteFromWatchlist завантажвае элемент і вяртае код 403, калі його userId не збігаецца з ID вызывача, чым адбываецца прыменэнне правіла, якое дазволяе выдалачу толькі власніку.
    import { Request, Response } from "express";
    import prisma from "../libs/prisma";
    
    // GET /watchlist - Mengambil semua watchlist milik user yang sedang login
    export const getWatchlist = async (req: Request, res: Response) => {
      try {
        const userId = (req as any).user.id;
    
        const watchlist = await prisma.watchList.findMany({
          where: { userId: userId },
          include: {
            movie: true, // Menyertakan informasi film
          },
        });
    
        return res.status(200).json(watchlist);
      } catch (error) {
        console.error("Get Watchlist Error:", error);
        return res.status(500).json({
          message: "Terjadi kesalahan pada server saat mengambil watchlist",
        });
      }
    };
    
    // POST /watchlist/:movieId - Menambahkan film ke watchlist user yang login
    export const addToWatchlist = async (req: Request, res: Response) => {
      try {
        const { movieId } = req.params;
        const userId = (req as any).user.id;
        const parsedMovieId = parseInt(movieId, 10);
    
        if (isNaN(parsedMovieId)) {
          return res.status(400).json({ message: "ID film tidak valid" });
        }
    
        // Periksa apakah film tersebut ada di katalog
        const movie = await prisma.movie.findUnique({
          where: { id: parsedMovieId },
        });
    
        if (!movie) {
          return res.status(404).json({ message: "Film tidak ditemukan" });
        }
    
        // Periksa apakah film sudah ada di watchlist user tersebut
        const existingWatchlist = await prisma.watchList.findUnique({
          where: {
            userId_movieId: {
              userId: userId,
              movieId: parsedMovieId,
            },
          },
        });
    
        if (existingWatchlist) {
          return res.status(400).json({
            message: "Film ini sudah ada di dalam daftar watchlist Anda",
          });
        }
    
        const newWatchlistItem = await prisma.watchList.create({
          data: {
            userId: userId,
            movieId: parsedMovieId,
            review: "",
            rating: 1,
          },
          include: {
            movie: true,
          },
        });
    
        return res.status(201).json({
          message: "Film berhasil ditambahkan ke watchlist",
          data: newWatchlistItem,
        });
      } catch (error) {
        console.error("Add to Watchlist Error:", error);
        return res.status(500).json({
          message: "Terjadi kesalahan pada server saat menambahkan watchlist",
        });
      }
    };
    
    // PATCH /watchlist/:id - Memperbarui review dan rating (siapa saja yang login bisa mengubah)
    export const updateWatchlist = async (req: Request, res: Response) => {
      try {
        const { id } = req.params;
        const { review, rating } = req.body;
        const watchlistId = parseInt(id, 10);
    
        if (isNaN(watchlistId)) {
          return res.status(400).json({ message: "ID watchlist tidak valid" });
        }
    
        // Periksa apakah item watchlist ada
        const watchlistItem = await prisma.watchList.findUnique({
          where: { id: watchlistId },
        });
    
        if (!watchlistItem) {
          return res.status(404).json({ message: "Item watchlist tidak ditemukan" });
        }
    
        // Validasi rating jika dikirimkan
        if (rating !== undefined) {
          const parsedRating = parseInt(rating, 10);
          if (isNaN(parsedRating) || parsedRating < 1 || parsedRating > 5) {
            return res.status(400).json({
              message: "Rating harus berupa angka antara 1 sampai 5",
            });
          }
        }
    
        const updatedItem = await prisma.watchList.update({
          where: { id: watchlistId },
          data: {
            review: review !== undefined ? review : undefined,
            rating: rating !== undefined ? parseInt(rating, 10) : undefined,
          },
          include: {
            movie: true,
          },
        });
    
        return res.status(200).json({
          message: "Watchlist berhasil diperbarui",
          data: updatedItem,
        });
      } catch (error) {
        console.error("Update Watchlist Error:", error);
        return res.status(500).json({
          message: "Terjadi kesalahan pada server saat memperbarui watchlist",
        });
      }
    };
    
    // DELETE /watchlist/:id - Menghapus film dari watchlist (hanya bisa oleh pembuatnya/Owner)
    export const deleteFromWatchlist = async (req: Request, res: Response) => {
      try {
        const { id } = req.params;
        const userId = (req as any).user.id;
        const watchlistId = parseInt(id, 10);
    
        if (isNaN(watchlistId)) {
          return res.status(400).json({ message: "ID watchlist tidak valid" });
        }
    
        // Periksa apakah item watchlist ada
        const watchlistItem = await prisma.watchList.findUnique({
          where: { id: watchlistId },
        });
    
        if (!watchlistItem) {
          return res.status(404).json({ message: "Item watchlist tidak ditemukan" });
        }
    
        // Validasi kepemilikan (Owner)
        if (watchlistItem.userId !== userId) {
          return res.status(403).json({
            message: "Anda tidak memiliki akses untuk menghapus item ini dari watchlist",
          });
        }
    
        await prisma.watchList.delete({
          where: { id: watchlistId },
        });
    
        return res.status(200).json({
          message: "Film berhasil dihapus dari watchlist Anda",
        });
      } catch (error) {
        console.error("Delete Watchlist Error:", error);
        return res.status(500).json({
          message: "Terjadi kesalahan pada server saat menghapus watchlist",
        });
      }
    };
    

    Есць два крайнія случая, якія трэба ведаць. Перакантрольванне на наявнасць дубліката і адчыненне новай ентры — гэта разны запиты, таму два адночасныя запросы могу оба праясці перакантрольванне; унікальныя абмежэння як і раней захоўваюць даны, але той, хто прағавіў, отрымае код 500, якщо толькі не паспеліце з’явіцься на адхыленне. А функцыя parseInt є паслабленай: "4.7" ператвараецца ў 4, а "3abc" — у 3, таму болей строгія перакантрольвання, такая як Number.isInteger(Number(rating)), болей надзеямо захоўваець ад некоректных даных.

    Падключэнне маршрутаў

    Слой маршрутаў складаецца з аднаго файла на кожны ресурс плюс індэкс. Маршруты аутантыкацыі размешчаюцца пад /auth, тады як /movies і /watchlist праходзячы через мідлвэр authentication у момент запуску, таму кожны працоўнік, який знаходзится пасля іх, захаваны без неабяжнага пераканальвання. Точка входу актывае CORS, парсуе тэлы JSON, размешчае все пад /api і праслухоўвае на порту 3000.

    authRoute.ts
    
    import { Router } from "express";
    import { login, register } from "../controllers/authController";
    
    const router = Router();
    
    router.post("/register", register);
    router.post("/login", login);
    
    export default router;
    
    index.ts
    
    import { Router } from "express";
    import authRoute from "./authRoute";
    import movieRoute from "./movieRoute";
    import watchlistRoute from "./watchlistRoute";
    import { authentication } from "../middlewares/authMiddleware";
    
    const route = Router();
    
    route.use("/auth", authRoute);
    route.use("/movies", authentication, movieRoute);
    route.use("/watchlist", authentication, watchlistRoute);
    
    export default route;
    
    movieRoute.ts
    
    import { Router } from "express";
    import { deleteMovie, getMovies } from "../controllers/movieController";
    
    const router = Router();
    
    router.get("/", getMovies);
    router.delete("/:id", deleteMovie);
    
    export default router;
    
    watchlistRoute.ts
    
    import { Router } from "express";
    import {
      getWatchlist,
      addToWatchlist,
      updateWatchlist,
      deleteFromWatchlist,
    } from "../controllers/watchlistController";
    
    const router = Router();
    
    router.get("/", getWatchlist);
    router.post("/:movieId", addToWatchlist);
    router.patch("/:id", updateWatchlist);
    router.delete("/:id", deleteFromWatchlist);
    
    export default router;
    
    
    isi main index.ts yang selevel dengan src:
    
    import express from "express";
    import route from "./routes";
    import cors from "cors";
    
    const app = express();
    const port = 3000;
    
    app.use(cors());
    app.use(express.json());
    app.use("/api", route);
    
    app.listen(port, () => {
      console.log(`Server is running on port ${port}`);
    });
    

    Чырваючы наявнасць гэтых прыфіксаў, рэальныя шляхі ёсць /api/auth/register, /api/movies і так далей, а не простыя шляхі, зазначаныя ў кантракце. Будзьце впэўнены, што кліент і будова API выкарыстоўваюць аднойчыны базовы URL.

    Ключавыя выводы

    • Явны модэль з’еднання з компузытным унікальным абмежэнням дае вам як захаванне ад дуплікацый, так і зручны ключ для пошуку.
  • Автанацыя павінна быць застосована адзін раз на рэверсе, а правілы автарызацыі, такія як можлівасць выдалення толькі аднакоў власніка, — у конкрэтным кантролеры.
  • Чыннае рашэнне па тым, як зникнуць залежныя рэкі: чераз транзакцыю ў коде чы рэакцыйную параду ў схеме.
  • У рашэнні з часовымі межамі першыя правкі зазвычай стосуюцца адрабаткі наявнасці праваў, а не выдачы первасовых памылак, запісу req.user і пераверкі зменных сераўысу пад час запуску.