
Im letzten Teil haben wir gemeinsam deinen Server vorbereitet und eine kleine Test-App erstellt, um sicherzustellen, dass dein Setup funktioniert. Heute gehen wir den nächsten großen Schritt: Wir bauen die Basis deines Backends.
Das Ziel dieses Artikels:
Dein NestJS-Backend bekommt eine richtige Struktur
Nutzer können sich über Firebase Authentication per ID-Token einloggen
Wir verbinden das Backend mit einer MySQL-Datenbank (über Sequelize)
Wir legen unsere erste Ressource an: ToDos (mit CRUD-Endpoints)
Klingt nach viel? Keine Sorge, wir gehen alles Schritt für Schritt durch, so dass du auch als Anfänger mitkommst. Am Ende dieses Artikels hast du bereits ein funktionierendes Backend, das deine Nutzer authentifizieren und ToDos speichern kann. 🚀
Sicherheits-Features selbst zu bauen ist fehleranfällig und extrem zeitaufwendig. Firebase bietet dir eine ausgereifte Lösung, die du mit wenigen Zeilen Code in dein NestJS-Backend integrieren kannst.
Du sparst Entwicklungszeit
Du profitierst von Googles Sicherheitsinfrastruktur
Du hast eine einfache Möglichkeit, User mit E-Mail/Passwort oder Social Logins anzumelden
Und Firebase Authentication ist komplett kostenlos, solange du keine Authentifizierung per Telefonnummer benötigst
Während Firebase auch eine eigene Datenbank (Firestore) anbietet, setzen wir hier bewusst auf MySQL. Warum?
MySQL ist relational → perfekt für ToDos, die oft Beziehungen wie User → ToDos abbilden
Du lernst, wie man eine klassische SQL-Datenbank mit NestJS integriert
Du hast die volle Kontrolle auf deinem VPS
So kombinierst du die Stärken von Firebase (Auth) mit der Flexibilität einer eigenen Datenbank (MySQL).
Firebase Authentication übernimmt für dich den ganzen komplizierten Part rund um Login, Passwort-Handling und Sicherheit. Du musst dich nicht um Hashing, Session-Management oder komplizierte Security kümmern – das alles macht Firebase für dich.
Gehe zu console.firebase.google.com
Erstelle ein neues Projekt → nenne es z. B. ToDo App
Aktiviere in den Einstellungen Authentication → Sign-in Method und wähle Email/Password
👉 Damit können sich Nutzer später einfach mit E-Mail & Passwort registrieren.
Anstatt alles von Hand einzubauen, nutzen wir das fertige Package nestjs-firebase-auth. Dort ist bereits ein Auth-Guard implementiert, den wir einfach einhängen können. Ein Auth-Guard ist sozusagen ein Wachposten, der deine API-Endpunkte bewacht. Um eine Anfrage an deinen Endpunkt zu senden, muss sich der Client (Website oder mobile App) zunächst authentifizieren. Das macht er normalerweise, indem er ein ID-Token Im Header der Anfrage mitschickt. Der Auth-Guard schaut sich dieses Token dann an und entscheidet, ob dem Nutzer der Zugriff gewährt wird.
npm install nestjs-firebase-auth firebase-adminIn deinem app.module.ts binden wir das Package ein:
import { Module } from '@nestjs/common';
import { FirebaseAdminModule } from '@alpha018/nestjs-firebase-auth';
import { ExtractJwt } from 'passport-jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
FirebaseAdminModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
base64: process.env.SERVICE_ACCOUNT_KEY,
options: {}, // Optionally, provide Firebase configuration here
auth: {
config: {
extractor: ExtractJwt.fromAuthHeaderAsBearerToken(), // Extract JWT from the header
checkRevoked: true, // Optionally check if the token is revoked
validateRole: true, // Enable role validation if needed
},
},
}),
inject: [ConfigService],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}Für die Implementierung unserer Firebase Admin SDK musst du dich mit einem Service Account anmelden. Das machen wir in diesem Fall mit einem base64-codierten String. Den Inhalt dieses Strings, holst du dir einfach aus deinem Firebase Projekt unter Projekt-Einstellungen → Service Accounts → Admin SDK → Neuen privaten Schlüssen generieren. Durch einen Klick auf den Button wird eine JSON-Datei heruntergeladen. Diese kannst du dann auf der Seite base64encode.org unter Encode als Datei einfügen. Danach solltest du eine Text-Datei mit dem Schlüssel erhalten. Diesen Schlüssel kannst du nun kopieren und in deinem NestJS-Projektverzeichnis eine neue Datei erstellen namens ".env". Dort fügst du dann folgenden Code ein und ersetzt xxx durch deinen generierten Schlüssel:
SERVICE_ACCOUNT_KEY=xxxDu kannst ab jetzt jeden deiner Endpunkte ganz einfach mit dem FirebaseGuard absichern. Wie du das machst, erkläre ich dir am Ende des Artikels, also bleib gespannt!
Wir brauchen eine Datenbank, um die ToDos der Nutzer dauerhaft zu speichern. Dafür nutzen wir in diesem Projekt MySQL, weil es schnell, zuverlässig und sehr verbreitet ist. Natürlich kannst du für dein eigenes Projekt auch jedes beliebige andere Datenbankmanagementsystem nutzen.
Zuerst erstellen wir einen MySQL Server auf unserem VPS. Dieser ist dann über einen bestimmten Port (normalerweise 3306) ansprechbar und ermöglicht es uns, Datenbankanfragen mit NestJS zu senden und zu empfangen.
sudo apt update
sudo apt install mysql-serverDamit ist dein MySQL-Server auch schon bereit. Als nächstes begibst du dich auf den Server:
sudo mysqlNun kannst du SQL-Befehle eingeben und somit neue Tabellen und Einträge erstellen. Wir wollen das aber nicht über das Terminal machen, sondern über unser NestJS-Backend. Für den Zugriff benötigen wir also einen Benutzer für den MySQL-Server, mit dem wir uns in NestJS anmelden können:
CREATE DATABASE todo_app;
CREATE USER 'todo_user'@'localhost' IDENTIFIED BY 'starkesPasswort123!';
GRANT ALL PRIVILEGES ON todo_app.* TO 'todo_user'@'localhost';
FLUSH PRIVILEGES;Natürlich solltest du das Passwort und deinen Benutzernamen selbst wählen. Doch damit ist dieser Schritt getan. Wir haben auf unserem VPS jetzt einen funktionierenden MySQL-Server und können darauf aus NestJS zugreifen!
Für den Zugriff verwenden wir Sequelize. Das ist ein echt praktisches Package für die Kommunikation mit einer MySQL-Datenbank. Durch Sequelize gehen wir nicht nur den lästigen SQL-Befehlen aus dem Weg, sondern können auch komplett dynamisch unsere Datenbankstruktur über NestJS anpassen. Begib dich wieder in dein NestJS-Projekt und gib den folgenden Befehl im Terminal ein:
npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2Zuerst einmal müssen wir unserer App sagen, wie sie sich auf unserem MySQL-Server anmelden kann. Dafür erweitern wir erstmal unsere .env-Datei:
SERVICE_ACCOUNT_KEY=xxx
MYSQL_HOST=[Deine Server-IP-Adresse]
MYSQL_PASSWORD=[Dein MySQL-Passwort]
MYSQL_USER=[Dein MySQL-Benutzername]
MYSQL_DATABASE=todo_appHier trägst du natürlich die für dich zutreffenden Daten ein. Als nächstes legen wir in unserem src-Ordner die Datei database.providers.ts an. Diese befüllen wir wie folgt:
import { Sequelize } from 'sequelize-typescript';
import { databaseDialect, databaseProvider } from './common/constants';
export const databaseProviders = [
{
provide: databaseProvider,
useFactory: async () => {
const sequelize = new Sequelize({
dialect: databaseDialect,
host: process.env.MYSQL_HOST,
port: 3306,
username: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE,
});
sequelize.addModels([]);
sequelize.sync({ alter: true })
.then(() => {
console.log('Database synchronized');
})
.catch((err) => {
console.error('Error synchronizing database:', err);
});
return sequelize;
},
},
];Wie du siehst, nutzen wir hier unsere vorher angelegten Variablen aus unserer .env-Datei, indem wir beispielsweise process.env.MYSQL_HOST nutzen. Zu guter Letzt erstellen wir ein Database Module in der Datei database.module.ts, die sich ebenfalls in unserem src-Ordner befindet.
import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';
@Module({
providers: [...databaseProviders],
exports: [...databaseProviders],
})
export class DatabaseModule {}Und damit haben wir es geschafft. NestJS kann jetzt auf unsere Datenbank zugreifen und den ersten ToDos steht nichts mehr im Weg!
Jetzt wird’s spannend: Wir bauen die eigentliche ToDo-API. Ziel ist es also mehrere Endpunkte für die Erstellung, Bearbeitung, Löschung und das Auslesen von ToDos zu implementieren.
NestJS bringt dafür einen praktischen Generator mit:
nest g resource todosNach der Bestätigung des Befehls, wirst du danach gefragt welches Transport-Layer du nutzen möchtest. Wähle hier Rest API. Dann wirst du gefragt, ob du CRUD (Create Read Update Delete) Endpunkte haben möchtest. Hier bestätigst du einfach mit Enter.
Nachdem wir den Befehl ausgeführt haben, legt NestJS automatisch mehrere Dateien und Ordner für uns an. Werfen wir also zuerst einen Blick darauf, welche Struktur entstanden ist und welche Komponenten dabei erstellt wurden:

Du wirst sehen, dass jetzt ein Service, ein Controller und ein Module angelegt wurden. Das Module ist dabei die „Organisationseinheit“ – hier werden alle Teile gebündelt, die zu den ToDos gehören. Der Controller ist dafür zuständig, die verschiedenen Endpunkte bereitzustellen, über die unsere App später mit den ToDos arbeiten kann. NestJS hat hier schon automatisch CRUD-Endpunkte (Create, Read, Update, Delete) für uns vorbereitet. Die eigentliche Logik, also das, was wirklich im Hintergrund passieren soll, landet aber im Service. Dort bauen wir später auch die Verbindung zu unserer Datenbank ein.
Zusätzlich wurden zwei Ordner erstellt: entities und dtos.
Im Entities-Ordner definieren wir unsere Datenmodelle – also die Struktur, wie ein ToDo-Objekt in der Datenbank aussieht. Man kann sich das wie ein Bauplan vorstellen: Welche Eigenschaften soll ein ToDo besitzen? Typischerweise braucht ein ToDo mindestens einen Titel, um zu wissen, was getan werden muss. Vielleicht auch ein Fälligkeitsdatum, damit klar ist, bis wann die Aufgabe erledigt werden soll. Diese Eigenschaften halten wir dann in unserem Entity als Variablen fest.
Die DTOs (Data Transfer Objects) sind ebenfalls Datenmodelle, haben aber eine andere Aufgabe. Sie beschreiben die Daten, die ein Client (z. B. unsere App oder Website) beim Anlegen oder Bearbeiten eines ToDos an unseren Server sendet. Ein DTO muss nicht zwangsläufig alle Eigenschaften des Entities enthalten, sondern nur die, die für die jeweilige Anfrage relevant sind. Praktisch ist auch: Mit DTOs können wir gleich sicherstellen, dass die übermittelten Daten sinnvoll sind. Zum Beispiel können wir prüfen, dass der Titel eines ToDos wirklich ein String ist und nicht leer bleibt. Sollte diese Bedingung nicht erfüllt sein, gibt NestJS automatisch einen Bad Request Error zurück – ganz ohne dass wir selbst viele komplizierte IF-ELSE-Abfragen schreiben müssen.
Jetzt ist es an der Zeit, unser erstes Model zu erstellen. Füge den folgenden Inhalt in die Datei todos/entities/todo.entity.ts ein:
import { Table, Column, Model, DataType } from 'sequelize-typescript';
@Table({
tableName: 'todos',
})
export class Todo extends Model {
@Column({
type: DataType.TEXT,
allowNull: false,
})
title: string;
@Column({
type: DataType.DATE,
allowNull: false,
})
dueDate: Date;
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
})
completed: boolean;
}Kurz zur Erklärung, was wir hier eigentlich machen. Mit @Table({ tableName: 'todos' }) sagen wir, dass in der Datenbank eine Tabelle namens todos erstellt bzw. verwendet werden soll.
Die Klasse Todo erbt von Model, was bedeutet, dass sie alle Funktionen von Sequelize bekommt, um später Daten in der Tabelle zu speichern, abzufragen oder zu löschen.
Innerhalb der Klasse definieren wir die Spalten (Columns):
title
Typ: TEXT
Darf nicht leer sein (allowNull: false)
Das ist der Titel unseres ToDos, also z. B. „Einkaufen gehen“.
dueDate
Typ: DATE
Darf ebenfalls nicht leer sein (allowNull: false)
Dieses Feld gibt an, bis wann ein ToDo erledigt sein soll.
completed
Typ: BOOLEAN (also true/false)
Standardwert: false
Dieses Feld gibt an, ob ein ToDo schon erledigt ist oder nicht.
Kurz gesagt: Mit dieser Datei beschreiben wir die Tabelle todos in der Datenbank und legen fest, dass jedes ToDo einen Titel und einen Status (erledigt oder nicht) haben soll. Wichtig zu wissen ist, dass Sequelize automatisch auch die Felder id, createdAt und updatedAt zu unserem Model hinzufügt. Das wirst du später in der Datenbank sehen, wenn du ein erstes ToDo erstellst.
Wie bereits erwähnt, sind Dtos ein einfacher Weg, um deine Anfragen zu validieren. Um das zu tun, müssen wir aber zunächst den class-validator installieren:
npm i --save class-validator class-transformerDann können wir unsere create-todo.dto.ts Datei wie folgt füllen:
import { IsDate, IsNotEmpty, IsOptional, IsString } from "class-validator";
export class CreateTodoDto {
@IsString()
@IsNotEmpty()
title: string;
@IsDate()
@IsNotEmpty()
dueDate: Date;
@IsOptional()
completed?: boolean;
}Damit zeigen wir unserem Endpunkt, welchen Body er zu erwarten hat. Also welche Werte muss ein Client wie übergeben, damit der Endpunkt das machen kann, was er soll. In diesem Fall legen wir das für unseren Create-Endpunkt fest, mit dem wir später ToDos erstellen werden. Class-Validator liefert unzählige Möglichkeiten, um die übergebenen Werte zu validieren. Probier dich gern mal etwas aus und teste verschiedene Einstellungen. Für unser Beispiel nutzen wir erstmal nur IsDate, IsNotEmpty, IsOptional und IsString.
Damit wir unser Todo-Model später im Service sauber nutzen können, legen wir einen eigenen Provider an. Provider sind in NestJS eine Art „Bausteine“, die man in andere Klassen einfügen kann. So bleibt der Code modular, wiederverwendbar und übersichtlich.
Dafür erstellen wir eine neue Datei todos.providers.ts mit folgendem Inhalt:
import { Todo } from './entities/todo.entity';
export const todosProviders = [
{
provide: todoProvider,
useValue: Todo,
},
];Wie dir nun sicherlich auffällt, fehlt uns noch die Variable todoProvider. Diese legen wir in der Datei src/common/constants.ts an. Dort können wir später auch weitere Provider-Schlüssel hinterlegen. Im Falle unseres todoProviders sieht das wie folgt aus:
export const todoProvider = 'TODO_REPOSITORY';Immer wenn wir in unserem Service nach TODO_REPOSITORY fragen, bekommen wir automatisch unser Todo-Model zurück.
Im nächsten Schritt müssen wir todosProviders auch wirklich im Projekt bekannt machen. In der Datei todos.module.ts müssen wir daher folgenden Code hinzufügen:
import { DatabaseModule } from 'src/database.module';
import { Todo } from './models/todo.model';
import { todosProviders } from './todos.providers';
@Module({
imports: [DatabaseModule],
controllers: [TodosController],
providers: [TodosService, ...todosProviders],
})
export class TodosModule {}Somit kennt unser TodosModule nun unsere Datenbank durch das DatabaseModule und gleichzeitig unsere todosProviders. Somit können wir diese jetzt im TodosService benutzen. Dazu müssen wir im Service aber folgende Anpassung vornehmen:
import { Inject, Injectable } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
import { Todo } from './entities/todo.entity';
@Injectable()
export class TodosService {
constructor(
@Inject(todoProvider)
private todosRepository: typeof Todo,
) {}
...Aber warum machen wir das überhaupt so? Wir trennen sauber die Zuständigkeiten (Separation of Concerns), unsere Services werden dadurch besser testbar und flexibler und wir nutzen die Stärke von NestJS: Dependency Injection. Das bedeutet, dass wir Bausteine wie unser Todo-Model überall dort einfügen können, wo wir sie brauchen.
Aktuell machen unsere Endpunkte noch nicht viel. Würden wir jetzt mit Postman beispielsweise unsere Todo-Endpunkte ansprechen, würden wir einfach nur verschiedene String zurückgegeben bekommen. Das wollen wir in diesem Schritt ändern.
Jeder unserer CRUD-Endpunkte braucht eine direkte Kommunikation zur Datenbank. Fangen wir am besten mit der Erstellung eines ToDos an. Hierfür passen wir die create-Funktion in unserem Service wie folgt an:
create(createTodoDto: CreateTodoDto) {
return this.todosRepository.create<Todo>({ ...createTodoDto });
}In unserem todos.service.ts haben wir die Funktion create, die dafür sorgt, dass ein neues ToDo in unserer Datenbank gespeichert wird. Die Funktion bekommt ein unser createTodoDto übergeben.
Über this.todosRepository greifen wir auf unsere Datenbank zu. Dieses Repository haben wir vorher als Provider eingebunden, damit wir es hier im Service nutzen können. Mit dem Befehl .create<Todo>() legen wir dann ein neues ToDo in der Datenbank an. Das <Todo> ist ein Hinweis für TypeScript, dass das Ergebnis dieser Operation ein ToDo-Objekt ist.
Spannend ist der Ausdruck { ...createTodoDto }. Die drei Punkte sind der sogenannte Spread-Operator. Er sorgt dafür, dass alle Eigenschaften, die in createTodoDto drin sind, direkt in unser neues Objekt übernommen werden. Wenn also beispielsweise vom Client die Daten { "title": "Einkaufen gehen", "completed": false } kommen, wird genau dieses Objekt auch an unsere Datenbank weitergegeben.
Am Ende geben wir mit return das Ergebnis dieser Operation zurück. Das heißt, sobald das neue ToDo erfolgreich gespeichert wurde, bekommt der Client das frisch angelegte ToDo-Objekt wieder zurück – meist als JSON.
Analog dazu füllen wir nun auch die anderen Funktionen unseres Services:
findAll() {
return this.todosRepository.findAll<Todo>();
}
findOne(id: number) {
return this.todosRepository.findOne<Todo>({ where: { id } });
}
update(id: number, updateTodoDto: UpdateTodoDto) {
return this.todosRepository.update<Todo>({ ...updateTodoDto }, { where: { id } });
}
remove(id: number) {
return this.todosRepository.destroy({ where: { id } });
}Du siehst, dass hier ein ähnlicher Aufbau verwendet wird. Der einzige Unterschied sind die WHERE-Abfragen. Diese sorgen schlichtweg dafür, dass wirklich nur der eine Eintrag mit der übergebenen ID zurückgegeben wird. Die ID ist der Primary Key unserer ToDos. Das bedeutet, dass jedes ToDo eindeutig über seine ID identifiziert werden kann. Somit eignet sie sich hervorragend für die Suche nach einzelnen ToDos.
Und damit haben wir es schon geschafft! Wir können die NestJS-App jetzt mit dem Befehl
npm run start:dev
ausführen und finden unsere Endpunkte dann unter http://localhost:3000/todos. Ich würde dir empfehlen, beispielsweise Postman zum Testen zu verwenden. Dort kannst du deine Endpunkte mit den verschiedenen Request-Arten ansprechen.
Solltest du den ersten Teil dieser Reihe bereits befolgt haben, kannst du auch wieder pm2 verwenden, um deine App zum Laufen zu bringen. Nutze dazu einfach den folgenden Befehl:
pm2 restart todo-apiEs gibt zwei Möglichkeiten, den Firebase Guard zu nutzen, um deine Endpunkte abzusichern. Wenn du nur einzelne Endpunkte mit einer Authentifizierung versehen möchtest, kann du in deinem todos.controller.ts einfach über den jeweiligen Endpunkt @UseGuards(FirebaseGuard) schreiben:
@UseGuards(FirebaseGuard)
@Post()
create(@Body() createTodoDto: CreateTodoDto) {
return this.todosService.create(createTodoDto);
}Falls du alle Endpunkte in einem Modul schützen möchtest, kannst du diese Zeile auch einfach über deinen Controller schreiben:
@UseGuards(FirebaseGuard)
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Post()
create(@Body() createTodoDto: CreateTodoDto) {
return this.todosService.create(createTodoDto);
}Somit können nur noch angemeldete Nutzer auf deine Endpunkte zugreifen. Für den Test mit Postman kannst du diese Zeile entweder rausnehmen oder du holst dir vorher über Postman dein Bearer Token, was du in den Header deiner Anfragen einfügen kannst. Dies erhältst du folgendermaßen:

Den Firebase API Key findest du in den Projekteinstellungen deines Firebase Projekts unter Web API Key. Die Anfrage sollte dir ein ID-Token zurückgeben, welches du für die Anfragen an deine NestJS-App nutzen kannst.
In diesem Artikel hast du richtig viel geschafft:
✅ Firebase Authentication für sichere Logins eingebunden
✅ MySQL mit Sequelize konfiguriert
✅ Erste Ressource (ToDos) mit NestJS-Generator erstellt
✅ API stabil mit pm2 gestartet
Damit hast du jetzt die Basis deines Backends: Nutzer können sich registrieren, einloggen und ihre eigenen ToDos speichern.
Im nächsten Artikel kümmern wir uns darum, wie wir die API clever mit unserer Flutter-App verbinden, sodass deine Nutzer bald ihre ToDos direkt am Smartphone managen können 📱.
Kommentare
Bitte melde dich an, um einen Kommentar zu schreiben.