Strona główna / Artykuły

Przewodnik po MovieVault: API lista obserwowanych filmów z użyciem Express 5, Prisma 7 i JWT

MovieVault – ćwiczenie typu full-stack z ograniczonym czasem realizacji: API do zarządzania listą filmów na Express 5, Prisma 7 i JWT, wraz z notatkami dotyczącymi weryfikacji praw własności, mechanizmów kaskadowych oraz obsługi błędów.

Wydawca: REACTAPP.TOP14 min czytania3198 słów

Źródła: PRISMA SCHEMA SIMPLE TOUCH (BACKEND)

Ćwiczenia z kodowania na żywo w określonym czasie to powszechny sposób oceny programistów full-stack, a one nagradzają kandydatów, którzy potrafią szybko przekształcić krótki opis w działającą, zabezpieczoną API. Ten przewodnik wykorzystuje właśnie taki krótki opis – katalog filmów o nazwie MovieVault z osobistą listą oglądanych filmów – i łączy go z kompletnym backendem opartym na Express, TypeScript i Prisma. Zobaczysz, jak model danych reprezentuje związek typu wiele-do-wielu, jak reguły autoryzacji i zarządzania prawami przenoszą się na mechanizmy middleware oraz sprawdzenia w kontrolerach, a także które niedoskonałości w rozwiązaniu realizowanym w ograniczonym czasie warto naprawić, zanim trafi ono do środowiska produkcyjnego.

Opis projektu MovieVault

Aplikacja umożliwia użytkownikom przeglądanie katalogu filmów, zapisywanie tytułów do osobistej listy oglądanych filmów oraz zarządzanie nimi. Wymagane funkcje to:

  • Rejestracja i logowanie.
  • Lista dostępnych filmów z możliwością usunięcia filmu z głównego katalogu.
  • Dodawanie filmu do osobistej listy oglądanych filmów użytkownika.
  • Modyfikowanie recenzji i oceny wpisu na liście oglądanych filmów. Jest to celowo proces współpracy: każdy zalogowany użytkownik może edytować dowolny wpis, a nie tylko osoba, która go stworzyła.
  • Usuwanie filmu z listy oglądanych filmów, co może wykonać tylko jej właściciel.
  • Serwer wykorzystuje Express, TypeScript i Prisma; klient korzysta z React, Tailwind i ShadCN UI.

    Zasady ogólne

    • Czas trwania zadania to 280 minut.
    • Praca musi być własnym twórczością użytkownika.
    • Katalog node_modules musi zostać wykluczony za pomocą pliku .gitignore, a plik .env.example musi zostać dodany do komitetu.
    • ShadCN UI jest obowiązkowy dla podstawowych komponentów takich jak Button, Card i Input.
    • Błędy muszą być obsługiwane na serwerze, a ich informacje muszą być pokazywane użytkownikowi po stronie klienta, przynajmniej za pomocą console.log lub komunikatu toast.

    Ustawianie serwera i modelu danych

    Serwer to projekt Express napisany w TypeScript z Prismą jako ORM. W specyfikacji wymagane są trzy modele: User (z id, które może być liczbą całkowitą lub łańcuchem, unikalnym email, password oraz name), Movie (id, title, genre, description, imageUrl) oraz model listy do oglądania (id, userId, movieId, łańcuch review z domyślnie pustym wartością oraz liczbę całkowitą rating z domyślną wartością 1). Użytkownicy i filmy są powiązani w relacji wiele-do-wielu za pośrednictwem tabeli listy do oglądania, a co najmniej pięć filmów musi zostać załadowanych z ręcznie napisanego pliku JSON znajdującego się w adresie server/data/movies.json.

    Schemat poniżej realizuje to poprzez wyraźny model łączenia. Dwie kwestie wymagają uwagi. Ograniczenie @@unique([userId, movieId]) oznacza, że użytkownik nie może dwa razy zapisywać tego samego filmu, a także dostarcza Prismie złożony klucz o nazwie userId_movieId, który kontrolery wykorzystują później do wyszukiwań. Blok datasource nie zawiera pola url: w Prismie 7 ciąg połączeniowy jest konfigurowany poza schematem (w pliku prisma.config.ts), a klient łączy się za pośrednictwem adaptera sterownika, dlatego w zależnościach pojawiają się @prisma/adapter-pg oraz pg. Pełne instrukcje konfiguracji znajdują się pod adresem konfiguracja Prismy 7 z PostgreSQL w projekcie 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])
    }
    

    Konfiguracja klienta

    Frontend to aplikacja React zbudowana przy użyciu Vite, stylizowana za pomocą Tailwind CSS i ShadCN UI, a zarządzanie trasami odbywa się za pomocą React Router. Specyfikacja łączy każdy etap na stronie serwera z odpowiednią ekranem klienta:

    • Strony /login i /register stworzone z komponentów formularzy ShadCN. Po udanym logowaniu token jest przechowywany, a użytkownik jest przekierowywany na panel sterowania pod adresem /.
    • Panel sterowania, który wyświetla filmy w postaci siatki karty ShadCN. Każda karta ma przyciski „Dodaj do listy obserwowanych” oraz „Usuń film”, a usunięcie musi natychmiast zaktualizować siatkę.
    • Strona /my-watchlist, która wyświetla zapisane przez użytkownika filmy wraz z ich recenzją i oceną.
    • Proces edycji pozycji na liście obserwowanych – albo za pomocą okna dialogowego ShadCN, albo na osobnej stronie – przy czym interfejs aktualizuje się natychmiast po pomyślnej zmianie.

    Umowa API

    • POST /register rejestruje użytkownika; nie wymaga autoryzacji.
    • POST /login zwraca token dostępu JWT; nie wymaga autoryzacji.
    • GET /movies wyświetla wszystkie filmy; wymaga autoryzacji.
    • DELETE /movies/:id usuwa film z katalogu; wymaga autoryzacji.
    • GET /watchlist pokazuje wpisy bieżącego użytkownika, wykorzystując include z Prismy do dodania szczegółów filmu; wymaga autoryzacji.
    • POST /watchlist/:movieId dodaje film do listy obserwowanych filmów bieżącego użytkownika; wymaga autoryzacji.
    • PATCH /watchlist/:id aktualizuje recenzję i ocenę bez sprawdzania właściciela; wymaga autoryzacji.
    • DELETE /watchlist/:id usuwa wpis; wymaga autoryzacji i jest dostępne tylko dla właściciela.

    Hasła muszą być hasowane za pomocą bcrypt.

    Kontroler autoryzacji obsługuje oba publiczne punkty końcowe. Funkcja register odrzuca żądanie z kodem błędu 400, gdy brakuje jakiegoś pola, hasło jest haszowane przy użyciu soli bcrypt o stopniu trudności 12, tworzony jest użytkownik, a następnie natychmiast wydawany jest token JWT zawierający id, name i email użytkownika, który wygasa po jednym dniu. Funkcja login wyszukuje użytkownika pod adresem e-mail, porównuje hasło za pomocą bcrypt.compare i zwraca ten sam typ tokena. Obie funkcje zwracają jedynie bezpieczne pola użytkownika, nigdy sam hasz.

    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,
        });
      }
    };
    

    Kwestie wymagające poprawy:

    • Podczas logowania używana jest jedna wiadomość dla nieznanych adresów e-mail oraz błędnych haseł, co uniemożliwia ujawnienie, jakie konta istnieją. Zwrócenie kodu błędu 401 zamiast 400 pozwoliłoby dokładniej opisać przyczynę błędu.
  • Rегистrowanie już istniejącej adresu e-mail powoduje naruszenie ograniczenia unikalności i skutkuje błędem typu 500. Wykrycie błędu naruszenia unikalności w Prismie i odpowiedź 409 dają klientowi coś, z czym może coś zrobić.
  • Oba bloki obsługi błędów wysyłają surowy obiekt error do klienta. Zapisz go na serwerze i zwróć tylko komunikat.
  • process.env.JWT_SECRET as string tłumi kompilator, ale nie czas wykonywania; jeśli zmienna jest brakująca, podpisywanie zawodzi przy każdej prośbie. Waliduj wymagane zmienne środowiskowe przy uruchamianiu.
  • Aby zobaczyć wzmocnioną wersję tego procesu, sprawdź autoryzację JWT gotową do użycia w API Node.js.

    Middleware autoryzacji

    Middlewarstwa odczytuje nagłówek Authorization, bierze część po pierwszej przestrzeni jako token i zwraca kod 401, gdy ten jest nieobecny. Następnie weryfikuje token za pomocą wspólnego sekretu i dołącza zdekodowany payload do żądania, aby kontrolery mogły odczytać id użytkownika. Nieprawidłowy lub wygasły token również powoduje kod 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" });
      }
    };
    

    Przekształcenie (req as any).user działa, ale eliminuje sprawdzanie typu we wszystkich kontrolerach, które je używają. Rozszerzenie interfejsu Request w Express poprzez łączenie deklaracji zapewnia typizację req.user. Parsowanie nagłówków przyjmuje również dowolny schemat, a nie tylko Bearer.

    Zależności projektu

    package.json określa wersje komponentów użytych w momencie tworzenia projektu: Express 5, Prisma 7 z adapterem PostgreSQL, bcrypt, jsonwebtoken i cors, a także TypeScript oraz ts-node-dev do uruchamiania skryptu dev, który restartuje projekt po zmianach. Wersje komponentów zmieniają się szybko, dlatego przed ponownym użyciem należy sprawdzić najnowsze wydania.

    {
      "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"
      }
    }
    

    Należy pamiętać, że żaden z elementów listy nie ładuje pliku .env do zmiennej process.env, więc potrzebny jest narzędzie takie jak dotenv lub obsługa plików konfiguracyjnych w Twoim środowisku wykonawczym, aby JWT_SECRET był dostępny.

    Koniecpunkty dotyczące filmów

    getMovies po prostu zwraca wszystkie filmy. deleteMovie analizuje parametr trasy, zwraca 400 w przypadku niecyfrowego ID oraz 404 przy nieznanych ID, a następnie uruchamia $transaction, który najpierw usuwa wszystkie wiersze listy obserwowanych filmów odnoszące się do danego filmu, a potem sam film. Usunięcie najpierw elementów zależnych zapobiega naruszeniu reguł klucza obcego, a transakcja gwarantuje, że oba kroki będą skuteczne albo nieudane jednocześnie.

    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",
        });
      }
    };
    

    Alternatywą jest ustawienie onDelete: Cascade na relacji movie listy obserwowanych filmów, co pozwala bazie danych automatycznie usunąć zależne wiersze. Wyraźna transakcja jest lepiej widoczna w kodzie, natomiast mechanizm kaskadowy trudniej zapomnieć.

    Koniecpunkty listy obserwowanych filmów

    Kontroler listy obserwowanych filmów zawiera najciekawsze reguły:

    • getWatchlist filtruje według ID użytkownika z tokena i używa include: { movie: true }, dzięki czemu każda pozycja przychodzi wraz ze swoim filmem.
    • addToWatchlist weryfikuje ID filmu, potwierdza jego istnienie, wyszukuje klucz złożony userId_movieId, aby odrzucić duplikaty z kodem błędu 400, a następnie tworzy pozycję z domyślną recenzją i oceną.
    • updateWatchlist wdraża zasadę współpracy: sprawdza, czy pozycja istnieje, ale nie kto jest jej właścicielem. Jeśli istnieje rating, musi zostać on przetłumaczony na liczbę całkowitą od 1 do 5. Pola pozostawione na undefined są ignorowane przez Prismę, więc częściowa aktualizacja dotyczy tylko tych danych, które zostały wysłane.
    • deleteFromWatchlist ładuje pozycję i zwraca błąd 403, gdy jej userId różni się od ID osoby wykonującej żądanie, co zapewnia przestrzeganie zasady dostępu tylko dla właściciela.
    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",
        });
      }
    };
    

    Dwa przypadki krawędziowe, o których należy wiedzieć. Sprawdzenie duplikatów i operacja wstawiania to oddzielne zapytania, więc dwa jednoczesne żądania mogą oba przejść tę kontrolę; ograniczenie unikalności nadal chroni dane, ale ten, który przegrywa, otrzymuje błąd 500, chyba że złapiesz to naruszenie. Ponadto parseInt jest łagodny: "4.7" staje się 4, a "3abc" – 3, więc bardziej rygorystyczna kontrola, taka jak Number.isInteger(Number(rating)), bardziej niezawodnie odrzuca niewłaściwe dane.

    Łączenie tras

    Kwarterowanie ma jeden plik na zasób plus indeks. Trasy autoryzacyjne są montowane pod adresem /auth, natomiast /movies i /watchlist przechodzą przez middleware authentication w momencie montowania, dzięki czemu każdy obsługiwanie zapytania za nimi jest chronione bez konieczności powtarzania sprawdzenia. Punkt wejścia umożliwia funkcjonowanie CORS, parsuje treści w formacie JSON, montuje wszystko pod adresem /api i nasłuchuje na porcie 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}`);
    });
    

    Z powodu tych przedrostków rzeczywiste ścieżki to /api/auth/register, /api/movies itp., a nie proste ścieżki opisane w kontrakcie. Upewnij się, że klient oraz wszelka dokumentacja API używają tej samej podstawowej adresu URL.

    Główne wnioski

    • Jasny model łączenia z złożonym ograniczeniem unikalności zapewnia zarówno ochronę przed duplikatami, jak i wygodny klucz do wyszukiwania.
  • Należy raz zastosować mechanizm autoryzacji na poziomie routera, a następnie określić zasady uprawnień, takie jak możliwość usuwania tylko przez właściciela, wewnątrz konkretnego kontrolera.
  • Należy celowo zdecydować, w jaki sposób będą znikać powiązane rekordy: poprzez transakcję w kodzie lub kaskadę w schemacie.
  • W rozwiązaniu ograniczonym czasowo pierwszymi krojkami naprawczymi są zazwyczaj obsługa naruszeń unikalności, unikanie wycieku surowych błędów, użycie req.user oraz weryfikacja zmiennych środowiskowych przy uruchamianiu.