Abschnitt 18 · Lektion 1
Projektstruktur
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
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:
| Schicht | Weiß etwas von | Weiß nichts von |
|---|---|---|
| Routen | HTTP, Statuscodes, req und res | SQL |
| Logik | den Regeln deiner Anwendung | HTTP und SQL |
| Datenzugriff | der Datenbank | HTTP 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
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("bibliothek.db");
/** Unten: nur SQL. Kein Rechnen, kein Urteil, keine Statuscodes. */
export function findeAlle() {
return db.prepare("SELECT id, titel, jahr FROM buecher ORDER BY id").all();
}
export function findeMitTitel(teil) {
return db
.prepare("SELECT id, titel, jahr FROM buecher WHERE titel LIKE '%' || ? || '%' ORDER BY id")
.all(teil);
} import { findeAlle, findeMitTitel } from "../daten/buecher.js";
/**
* Die Mitte: hier steht die Regel, was ein Buch fuer den Aufrufer ist.
* Diese Funktion laesst sich ohne Server aufrufen, und genau das macht
* sie testbar (Abschnitt 17).
*/
export function sucheBuecher(q) {
const zeilen = q ? findeMitTitel(q) : findeAlle();
const jetzt = new Date().getFullYear();
return zeilen.map((buch) => ({ ...buch, alt: jetzt - buch.jahr > 10 }));
} 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
import { findeAlle, findeMitTitel } from "./daten/buecher.js";
// Sieht harmlos aus und ist der Anfang vom Ende: die Logikfunktion
// bekommt res und antwortet selbst.
export function sucheBuecherUndAntworte(q, res) {
const zeilen = q ? findeMitTitel(q) : findeAlle();
if (zeilen.length === 0) {
return res.status(404).json({ fehler: "Nichts gefunden." });
}
res.json({ anzahl: zeilen.length, buecher: zeilen });
}
// Drei Folgen, alle unangenehm:
//
// 1. Du kannst sie nicht mehr testen, ohne ein res zu erfinden.
// 2. Du kannst sie nicht mehr von der Kommandozeile aus benutzen,
// nicht aus einem Cron-Job und nicht aus einer zweiten Route.
// 3. Der Statuscode, also eine HTTP-Entscheidung, steht jetzt in
// einer Datei, die von HTTP nichts wissen sollte. 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, kostenlosZu 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.