mitmario.dev

Projektstruktur

Node.js Sandbox 4 Min Lesezeit 3 BeispieleLektion 1 von 7

Siebzehn Abschnitte lang ging es darum, die Bausteine zu bauen. Diese Lektion beantwortet die Frage, die danach kommt und die kein Tutorial stellt: wohin damit?

Der ehrliche Grund, warum das zählt, ist nicht Schönheit. Es ist der Moment, in dem du nach drei Monaten eine Kleinigkeit ändern sollst und erst einmal zwanzig Minuten suchst, wo die Preisberechnung steht. Eine Struktur ist eine Verabredung mit deinem zukünftigen Ich, und die kostet heute fünf Minuten.

Drei Schichten, drei Aufgaben

Alles in einer Datei
import express from "express";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("bibliothek.db");

// Damit die Datei fuer sich laeuft: drei Buecher, jedes Mal frisch.
db.exec("DROP TABLE IF EXISTS buecher");
db.exec("CREATE TABLE buecher (id INTEGER PRIMARY KEY, titel TEXT, jahr INTEGER)");
db.exec(
  "INSERT INTO buecher (titel, jahr) VALUES " +
    "('Nachtschicht mit Node', 2011), " +
    "('Streams verstehen', 2019), " +
    "('Express im Alltag', 2024)"
);

const app = express();

app.get("/", (req, res) => {
  res.type("html").send(
    '<h1>Bibliothek</h1><p><a href="/api/buecher">/api/buecher</a></p>'
  );
});

app.get("/api/buecher", (req, res) => {
  const q = typeof req.query.q === "string" ? req.query.q.trim() : "";

  if (q.length > 100) {
    return res.status(400).json({ fehler: "Der Suchbegriff ist zu lang." });
  }

  const zeilen = q
    ? db.prepare("SELECT id, titel, jahr FROM buecher WHERE titel LIKE '%' || ? || '%' ORDER BY id").all(q)
    : db.prepare("SELECT id, titel, jahr FROM buecher ORDER BY id").all();

  // Und hier faengt es an: die Regel, was ein Buch fuer den Aufrufer
  // ist, steht mitten in einer Route und nirgends sonst.
  const buecher = zeilen.map((buch) => ({
    ...buch,
    alt: new Date().getFullYear() - buch.jahr > 10,
  }));

  res.json({ anzahl: buecher.length, buecher });
});

app.listen(3000);

Diese Route ist nicht schlecht geschrieben. Sie macht nur drei Dinge auf einmal, und deshalb kannst du keins davon einzeln anfassen. Sie nimmt eine Anfrage entgegen und antwortet (Routen), sie entscheidet, wann ein Buch als alt gilt (Logik), und sie spricht SQL (Datenzugriff).

Das Beispiel läuft. Im Reiter „Browser” steht ein Link, und dahinter liegt das JSON: alt ist bei dem Buch von 2011 wahr und bei den beiden jüngeren falsch. Genau diese eine Zeile ist die Regel, um die es gleich geht. Was drumherum steht, gehört nicht zur Geschichte: Die drei db.exec-Zeilen legen die Tabelle an, damit die Datei für sich läuft, und die Route auf / liefert nur den Link.

Die Aufteilung, die sich in fast jedem Projekt bewährt hat, folgt genau diesen drei Aufgaben:

SchichtWeiß etwas vonWeiß nichts von
RoutenHTTP, Statuscodes, req und resSQL
Logikden Regeln deiner AnwendungHTTP und SQL
Datenzugriffder DatenbankHTTP und den Regeln

Die Richtung ist dabei wichtiger als die Namen: oben ruft unten auf, nie umgekehrt. Eine Datenzugriffs-Datei, die etwas aus routen/ importiert, ist immer ein Fehler.

Die Mitte ist die, die man testen kann

Dieselbe Anwendung in drei Schichten
import express from "express";
import { sucheBuecher } from "../logik/buecher.js";

export const buecherRouter = express.Router();

/** Oben: entgegennehmen, pruefen, weitergeben, antworten. Sonst nichts. */
buecherRouter.get("/", (req, res) => {
  const q = typeof req.query.q === "string" ? req.query.q.trim() : "";

  if (q.length > 100) {
    return res.status(400).json({ fehler: "Der Suchbegriff ist zu lang." });
  }

  const buecher = sucheBuecher(q);
  res.json({ anzahl: buecher.length, buecher });
});

Derselbe Code, nur an drei Stellen. Interessant ist die mittlere Datei: sucheBuecher(q) nimmt eine Zeichenkette und gibt ein Array zurück. Mehr braucht sie nicht, und deshalb kannst du sie in einem Test aufrufen, ohne einen Server zu starten. Genau das war in Lektion 17.2 die Voraussetzung dafür, dass Tests überhaupt etwas taugen.

Das ist der eigentliche Nutzen der Trennung, und er ist kein Nebeneffekt: die Schicht mit den Regeln ist die, in der die Fehler stecken, und die Trennung ist das, was sie prüfbar macht.

Warum req und res nicht nach unten wandern

Wenn res in die Mitte rutscht
import { findeAlle, findeMitTitel } from "./daten/buecher.js";
import { NichtGefunden } from "./fehler.js";

// Dieselbe Regel, ohne res: die Funktion gibt zurueck oder wirft.
export function sucheBuecher(q) {
  const zeilen = q ? findeMitTitel(q) : findeAlle();

  if (zeilen.length === 0) throw new NichtGefunden("Nichts gefunden.");

  return zeilen;
}

// Die Route uebersetzt das dann in HTTP, und nur sie:
//
//   try {
//     res.json({ anzahl: ..., buecher: sucheBuecher(q) });
//   } catch (fehler) {
//     next(fehler);
//   }

Das passiert selten aus Absicht und fast immer aus Bequemlichkeit: Die Logikfunktion braucht einen Sonderfall, res ist gerade zur Hand, und schon steht ein Statuscode in einer Datei, die von HTTP nichts wissen sollte.

Die Regel dagegen ist kurz: eine Funktion in der Mitte nimmt Werte und gibt Werte zurück, oder sie wirft. Das Übersetzen in Statuscodes macht die Route, und dafür hast du in Abschnitt 16 den Fehler-Handler gebaut, der genau das zentral erledigt.

Nach Zuständigkeit oder nach Technik

Es gibt zwei Arten, Ordner zu schneiden. Nach Technik heißt routen/, logik/, daten/, also so wie oben. Nach Zuständigkeit heißt buecher/, nutzer/, ausleihen/, und in jedem Ordner liegen Route, Logik und Datenzugriff nebeneinander.

Für kleine Projekte ist die erste Variante die richtige, und zwar aus einem unspektakulären Grund: Bei drei Themen und neun Dateien findest du alles auch so, und drei Ordner sind weniger zu erklären als neun. Das dreht sich, sobald ein Bereich so groß wird, dass du beim Arbeiten dauernd zwischen drei Ordnern hin und her springst. Dann wandert er in einen eigenen Ordner mit allem darin, und der Rest bleibt, wie er ist. Diese Mischung ist kein Kompromiss aus Faulheit, sondern die übliche Form in gewachsenen Projekten.

Und dann ist da noch utils.js

Irgendwann entsteht eine Datei namens utils.js oder helpers.js, und ein halbes Jahr später stehen dreißig Funktionen darin, die nichts miteinander zu tun haben. Der Name ist das Problem: Er sagt nicht, was drinsteht, also passt alles hinein.

Das ist derselbe Gedanke wie in Lektion 2.6. Eine Datei bekommt einen Namen, der eine Aufgabe beschreibt, und wenn du keinen findest, ist das ein Hinweis und kein Grund, utils zu nehmen. datum.js, geld.js, slug.js sind drei Dateien statt einer und trotzdem übersichtlicher.

Was noch dazugehört und meist keine Diskussion auslöst: Konfiguration liegt in einer eigenen Datei (die baust du in der nächsten Lektion), Tests liegen neben dem, was sie prüfen, oder in einem eigenen tests/, und statische Dateien liegen in public/ und werden von express.static ausgeliefert.

Zum Mitnehmen

Eine Struktur ist keine Ordnung um der Ordnung willen. Sie ist die Antwort auf die Frage, wo etwas steht, und die stellt sich nicht heute, sondern in drei Monaten.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, auf dem Server geprüft

    Öffnet sich mit dem Basis Konto.