Startseite / Artikel

MovieVault Walkthrough: Eine Watchlist-API mit Express 5, Prisma 7 und JWT

MovieVault – eine zeitbasierte Full-Stack-Übung: Eine Watchlist-API mit Express 5, Prisma 7 und JWT, inklusive Anmerkungen zu Eigentumsprüfungen, Kaskaden und Fehlerbehandlung.

Verlag: REACTAPP.TOP14 Min. Lesezeit3198 Wörter

Quellen: PRISMA SCHEMA SIMPLE TOUCH (BACKEND)

Zeitgesteuerte Live-Coding-Übungen sind eine gängige Methode, um Full-Stack-Entwickler zu bewerten, und belohnen Kandidaten, die in der Lage sind, eine kurze Spezifikation schnell in eine funktionierende, sichere API umzuwandeln. In dieser Anleitung wird eine solche kurze Spezifikation verwendet – ein Filmkatalog namens MovieVault mit einer persönlichen Wunschliste – und sie mit einem vollständigen Backend aus Express, TypeScript und Prisma kombiniert. Sie werden sehen, wie das Datenmodell eine viele-zu-vielen-Beziehung darstellt, wie Authentifizierungs- und Eigentumsregeln in Middleware- sowie Controller-Prüfungen umgesetzt werden, und welche Unzulänglichkeiten in einer zeitbegrenzten Lösung vor dem Einsatz in der Produktion behoben werden sollten.

Die Spezifikation für MovieVault

Die Anwendung ermöglicht es Benutzern, durch einen Filmkatalog zu blättern, Titel in eine persönliche Wunschliste zu speichern und beides zu verwalten. Die erforderlichen Funktionen sind:

  • Registrierung und Anmeldung.
  • Eine Liste der verfügbaren Filme mit der Möglichkeit, einen Film aus dem Hauptkatalog zu löschen.
  • Hinzufügen eines Films zur persönlichen Wunschliste des Benutzers.
  • Ändern der Rezension und Bewertung eines Eintrags in der Wunschliste. Dies ist absichtlich kollaborativ: Jeder angemeldete Benutzer kann jeden Eintrag bearbeiten, nicht nur die Person, die ihn erstellt hat.
  • Löschen eines Films aus einer Wunschliste, was nur der Eigentümer des Eintrags tun kann.
  • Der Server verwendet Express, TypeScript und Prisma; der Client nutzt React, Tailwind und ShadCN UI.

    Grundregeln

    • Die Zeitbeschränkung beträgt 280 Minuten.
    • Die Arbeit muss eigene Arbeit sein.
    • node_modules muss über .gitignore ausgeschlossen werden, und eine .env.example-Datei muss eingereicht werden.
    • ShadCN UI ist für Kernkomponenten wie Button, Card und Input zwingend erforderlich.
    • Fehler müssen auf dem Server behandelt werden und mindestens über console.log oder eine Benachrichtigung dem Benutzer auf der Client-Seite angezeigt werden.

    Einrichtung des Servers und des Datenmodells

    Der Server ist ein Express-Projekt, das in TypeScript geschrieben wurde und Prisma als ORM verwendet. In der Anfrage werden drei Modelle gefordert: User (eine id, die entweder eine Ganzzahl oder ein String sein kann, eine eindeutige email, password und name), Movie (id, title, genre, description, imageUrl) sowie ein Modell für die Wunschliste (id, userId, movieId, ein review-String, der standardmäßig leer ist, sowie eine Ganzzahl rating, die standardmäßig 1 beträgt). Benutzer und Filme stehen über die Tabelle der Wunschliste in einer many-to-many-Beziehung zueinander, und mindestens fünf Filme müssen aus einer manuell erstellten JSON-Datei unter server/data/movies.json eingefügt werden.

    Das untenstehende Schema implementiert dies als explizites Join-Modell. Zwei Aspekte verdienen besondere Aufmerksamkeit. Die Einschränkung @@unique([userId, movieId]) bedeutet, dass ein Benutzer denselben Film nicht zweimal speichern kann, und sie gibt Prisma außerdem eine zusammengesetzte Schlüsselstruktur mit dem Namen userId_movieId zur Verfügung, die die Controller später für Abfragen nutzen. Der datasource-Block enthält keinen url: In Prisma 7 wird die Verbindungszeichenkette außerhalb des Schemas (in prisma.config.ts) konfiguriert, und der Client verbindet sich über einen Adapter – deshalb erscheinen @prisma/adapter-pg sowie pg in den Abhängigkeiten. Für die vollständigen Konfigurationsschritte siehe die Einrichtung von Prisma 7 mit PostgreSQL in einem TypeScript Node.js-Projekt.

    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])
    }
    

    Einrichtung des Clients

    Die Frontend-Plattform ist eine React-Anwendung, die mit Vite aufgebaut wurde, mit Tailwind CSS und ShadCN UI gestylt sowie mit React Router geroutet ist. Die Spezifikation verknüpft jeden Server-Meilenstein mit einem Client-Bildschirm:

    • /login und /register-Seiten, die aus ShadCN-Formkomponenten erstellt wurden. Nach einem erfolgreichen Anmelden wird das Token gespeichert und der Benutzer auf das Dashboard bei / umgeleitet.
    • Ein Dashboard, das Filme als Gitter aus ShadCN-Karten anzeigt. Jede Karte verfügt über einen „Zur Wunschliste hinzufügen“- und einen „Film löschen“-Button, wobei das Löschen sofort das Gitter aktualisieren muss.
    • Eine /my-watchlist-Seite, die die vom Benutzer gespeicherten Filme zusammen mit der Rezension und Bewertung jeder Eintragung auflistet.
    • Ein Bearbeitungsprozess für Einträge in der Wunschliste – entweder über ein ShadCN-Dialogfeld oder eine separate Seite – wobei die Benutzeroberfläche sofort aktualisiert wird, sobald die Änderung erfolgreich ist.

    Das API-Vertragswerk

    • POST /register registriert einen Benutzer; es erfolgt keine Authentifizierung.
    • POST /login gibt ein JWT-Zugriffstoken zurück; es erfolgt keine Authentifizierung.
    • GET /movies listet alle Filme auf; Authentifizierung erforderlich.
    • DELETE /movies/:id entfernt einen Film aus dem Katalog; Authentifizierung erforderlich.
    • GET /watchlist listet die Einträge des aktuellen Benutzers auf und verwendet Prismas include, um Filmdetails anzuhängen; Authentifizierung erforderlich.
    • POST /watchlist/:movieId fügt einen Film zur Watchlist des aktuellen Benutzers hinzu; Authentifizierung erforderlich.
    • PATCH /watchlist/:id aktualisiert Rezensionen und Bewertungen ohne Überprüfung des Eigentümers; Authentifizierung erforderlich.
    • DELETE /watchlist/:id entfernt einen Eintrag; Authentifizierung erforderlich und nur der Eigentümer darf dies tun.

    Passwörter müssen mit bcrypt gehässt werden.

    Anmeldung und Login

    Der Auth-Controller kümmert sich um beide öffentlichen Endpunkte. register lehnt eine Anfrage mit 400 ab, wenn ein Feld fehlt, hasht das Passwort mit einem bcrypt-Salz der Stärke 12, erstellt den Benutzer und signiert sofort ein JWT, das die id, name und email des Benutzers enthält und nach einem Tag abläuft. login sucht den Benutzer über die E-Mail, vergleicht das Passwort mit bcrypt.compare und gibt dasselbe Token zurück. Beide Handler geben nur sichere Benutzerfelder aus, niemals den Hash.

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

    Aspekte, die verbessert werden sollten:

    • Beim Login wird für eine unbekannte E-Mail sowie ein falsches Passwort dieselbe Meldung verwendet, was verhindert, dass bekannt wird, welche Konten existieren. Die Rückgabe von 401 anstelle von 400 würde den Fehler genauer beschreiben.
  • Die Registrierung einer bereits existierenden E-Mail verletzt die Eindeutigkeitsbeschränkung und führt zu einem allgemeinen 500-Fehler. Durch das Erfassen des Prisma-Eindeutigkeitsverstoß-Fehlers und die Antwort mit 409 erhält der Client handlungsrelevante Informationen.
  • Sowohl die Fehlerbehandlungsblocks senden das rohe error-Objekt an den Client. Protokollieren Sie es auf dem Server und geben Sie nur eine Nachricht zurück.
  • process.env.JWT_SECRET als String schweigt den Compiler, nicht jedoch die Laufzeit; fehlt die Variable, scheitert die Signierung bei jeder Anfrage. Überprüfen Sie die erforderlichen Umgebungsvariablen beim Start.
  • Für eine verbesserte Version dieses Ablaufs siehe JWT-Authentifizierung für die Produktion in Node.js-APIs.

    Das Authentifizierungs-Middleware

    Das Middleware liest den Authorization-Header, nimmt den Teil nach dem ersten Leerzeichen als Token und gibt 401 zurück, wenn dieses fehlt. Anschließend überprüft es das Token mit dem gemeinsamen Geheimnis und fügt die decodierte Nutzlast der Anfrage hinzu, damit Controller die id-Werte des Benutzers lesen können. Ein ungültiges oder abgelaufenes Token führt ebenfalls zu 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" });
      }
    };
    

    Die Typumwandlung (req as any).user funktioniert zwar, vernachlässigt aber die Typüberprüfung in jedem Controller, der sie verwendet. Durch das Erweitern der Request-Schnittstelle von Express mittels Declaration Merging bleibt req.user typisiert. Die Analyse des Headers akzeptiert außerdem jedes Schema, nicht nur Bearer.

    Projektabhängigkeiten

    Das package.json fixiert den Stack zum Zeitpunkt der Erstellung: Express 5, Prisma 7 mit dem PostgreSQL-Adapter, bcrypt, jsonwebtoken und cors, sowie TypeScript und ts-node-dev für ein dev-Skript, das bei Änderungen neu gestartet wird. Versionsspannen ändern sich schnell, daher sollten Sie vor der Wiederverwendung die aktuellen Veröffentlichungen überprüfen.

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

    Beachten Sie, dass nichts in der Liste eine .env-Datei in process.env lädt, sodass Sie etwas wie dotenv oder die Unterstützung für Env-Dateien Ihres Laufzeitumfelds benötigen, damit JWT_SECRET verfügbar ist.

    Film-Endpunkte

    getMovies gibt einfach alle Filme zurück. deleteMovie analysiert den Route-Parameter, gibt 400 für eine nicht numerische ID sowie 404 für eine unbekannte ID aus und führt anschließend eine $transaction durch, die zunächst alle Zeilen der Wunschliste löscht, die auf den Film verweisen, und danach den Film selbst. Das Vorher-Löschen der Kinddaten verhindert einen Foreign-Key-Verstoß, und die Transaktion sorgt dafür, dass beide Schritte entweder gemeinsam erfolgreich oder gemeinsam fehlschlagen.

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

    Eine Alternative besteht darin, auf der movie-Beziehung der Wunschliste onDelete: Cascade zu deklarieren, wodurch die Datenbank die abhängigen Zeilen automatisch entfernt. Die explizite Transaktion ist im Code besser sichtbar; das Cascade-Verhalten ist hingegen schwieriger zu übersehen.

    Endpunkte der Wunschliste

    Der Controller der Wunschliste enthält die interessantesten Regeln:

    • getWatchlist filtert nach der Benutzer-ID aus dem Token und verwendet include: { movie: true }, sodass jede Eintragung zusammen mit ihrem Film übergeben wird.
    • addToWatchlist prüft die Film-ID, stellt sicher, dass der Film existiert, sucht nach dem Kombinationsschlüssel userId_movieId, um Duplikate mit einer 400-Fehlermeldung abzulehnen, und erstellt die Eintragung mit der Standardbewertung und -bewertung.
    • updateWatchlist implementiert die gemeinsame Regel: Es prüft, ob die Eintragung existiert, aber nicht, wer sie besitzt. Falls eine rating vorliegt, muss diese in eine Ganzzahl von 1 bis 5 umgewandelt werden. Von Prisma als undefined hinterlegte Felder werden ignoriert, sodass eine teilweise Aktualisierung nur die übermittelten Daten betrifft.
    • deleteFromWatchlist lädt die Eintragung und gibt 403 zurück, wenn ihre userId von der des Aufrufers abweicht – dadurch wird die Regel, dass nur der Eigentümer löschen darf, durchgesetzt.
    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",
        });
      }
    };
    

    Zwei Randfälle, die man kennen sollte. Die Prüfung auf Duplikate und das Einfügen sind getrennte Abfragen, sodass zwei gleichzeitige Anfragen beide die Prüfung bestehen können; die eindeutige Beschränkung schützt die Daten dennoch, doch der Verlierer erhält eine 500-Fehlermeldung, es sei denn, man erkennt die Verletzung. Zudem ist parseInt nachsichtig: "4.7" wird zu 4 und "3abc" zu 3, weshalb eine strengere Prüfung wie Number.isInteger(Number(rating)) fehlerhafte Eingaben zuverlässiger ablehnt.

    Verbindung der Routen

    Die Routing-Schicht enthält eine Datei pro Ressource sowie einen Index. Authentifizierungs-Endpunkte sind unter /auth aufgebaut, während /movies und /watchlist zum Zeitpunkt der Einrichtung durch das authentication-Middleware geleitet werden, sodass jeder dahinterliegende Handler ohne wiederholte Überprüfung geschützt ist. Der Einstiegspunkt aktiviert CORS, parsert JSON-Körper, montiert alles unter /api und lauscht auf Port 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}`);
    });
    

    Wegen dieser Präfixe sind die tatsächlichen Pfade /api/auth/register, /api/movies usw., und nicht die einfachen Pfade aus dem Vertrag. Stellen Sie sicher, dass der Client sowie alle API-Dokumentationen dieselbe Basis-URL verwenden.

    Haupterkenntnisse

    • Ein explizites Verknüpfungsmodell mit einer zusammengesetzten eindeutigen Beschränkung bietet sowohl Schutz vor Duplikaten als auch eine praktische Suchschlüssel.
  • Setzen Sie die Authentifizierung einmal auf Router-Ebene um und definieren Sie anschließend die Autorisierungsregeln, wie beispielsweise Löschungen nur für den Eigentümer, innerhalb des spezifischen Controllers.
  • Entscheiden Sie bewusst, wie die abhängigen Zeilen verschwinden sollen: durch eine Transaktion im Code oder durch eine Kaskade im Schema.
  • In einer zeitlich begrenzten Lösung sind die ersten Korrekturen in der Regel die Handhabung von Eindeutigkeitsverletzungen, nicht das Durchsickern unverarbeiteter Fehler, das Auswählen von req.user sowie die Überprüfung der Umgebungsvariablen beim Starten.