RESTful-APIs mit Node.js, Express und TypeScript erstellen
Erstellen Sie eine API zur administrativen Verwaltung von Benutzerprofilen mit Express, TypeScript und PostgreSQL. Erstellen, lesen, aktualisieren und löschen Sie anschließend ein Profil per HTTP. Sie generieren lokale Test-Token und starten die API neu, um zu prüfen, ob das Profil erhalten bleibt. Die API speichert Profildaten; sie implementiert weder eine Anmeldung noch die Speicherung von Passwörtern.
Einführung
Die API richtet sich an vertrauenswürdige Administratoren mit signierten Zugriffs-Token.
Ein Token benötigt den Scope profiles:read zum Lesen von Profilen und
profiles:write zum Erstellen, Bearbeiten oder Löschen.
Diese Scopes berechtigen zum Zugriff auf alle Profile; dies ist keine Selfservice-API für
Benutzer. Der vertrauenswürdige Aussteller vergibt die Scopes. Anfragekörper können keine
Identität oder Rolle für die Autorisierung wählen.
Systemvoraussetzungen
Verwenden Sie Bash unter Linux, Node.js 24.21.0, Corepack, cURL und eine laufende Docker Engine. Corepack führt die im Projekt festgelegte Version Yarn 4.12.0 ohne globale Yarn-Installation aus. Das Beispiel verwendet PostgreSQL 17.11, Express 5.2.1, TypeORM 1.1.1 und TypeScript 5.9.3. Node.js 24 ist eine gepflegte LTS-Version. Express 5 leitet abgelehnte Promises aus Routen-Handlern an die Fehler-Middleware weiter.
Der folgende lokale Signierer liefert RS256-JWTs mit Aussteller, Zielgruppe, Subjekt, Ablaufzeitpunkt und durch Leerzeichen getrennten Scopes. Damit können Sie eine echte Signaturprüfung ohne Konto bei einem Identitätsanbieter erproben. Jeder, der seinen privaten Schlüssel besitzt, kann Administratorzugriff gewähren. Verwenden Sie ihn daher nur für diese Loopback-Demonstration.
Umgebung einrichten
Führen Sie die Einrichtungsschritte der Reihe nach aus und brechen Sie bei jedem Fehler ab.
Falls stack-api bereits existiert, wählen Sie ein anderes Verzeichnis,
statt es zu überschreiben. Die Subshell behält bei einem Einrichtungsfehler Ihr ursprüngliches
Verzeichnis bei:
(
mkdir stack-api && cd stack-api &&
mkdir -p src/config src/controllers src/middleware src/models src/routes src/local
) && cd stack-api
Speichern Sie diese vollständige Datei package.json:
{
"name": "stack-api",
"private": true,
"type": "module",
"packageManager": "yarn@4.12.0",
"scripts": {
"build": "tsc --project tsconfig.json",
"start": "node dist/index.js"
},
"dependencies": {
"express": "5.2.1",
"helmet": "8.3.0",
"jose": "6.2.12",
"pg": "8.23.0",
"reflect-metadata": "0.2.2",
"typeorm": "1.1.1",
"zod": "3.25.76"
},
"devDependencies": {
"@types/express": "5.0.6",
"@types/node": "24.10.1",
"typescript": "5.9.3"
}
}
Speichern Sie .yarnrc.yml, um eine projektlokale Installation von
node_modules zu verwenden:
nodeLinker: node-modules
enableGlobalCache: false
enableTelemetry: false
ignorePath: true
injectEnvironmentFiles: []
Erstellen Sie tsconfig.json. Explizite Angaben für
types und typeRoots verhindern, dass der Compiler
projektfremde ambiente Typen aus einem übergeordneten Projekt einbezieht:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"types": ["node"],
"typeRoots": ["./node_modules/@types"],
"strict": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*.ts"]
}
Führen Sie nun die Installation aus. Eine leere Datei yarn.lock legt dieses
Verzeichnis als separates Yarn-Projekt fest, auch in einem
Workspace. Das Leeren von NODE_OPTIONS verhindert, dass der Plug’n’Play-Loader
eines übergeordneten Projekts in diese Befehle eingeschleust wird:
touch yarn.lock && NODE_OPTIONS='' corepack yarn install
Bewahren Sie die generierte Lockdatei für spätere Installationen auf. Nehmen Sie lokale
Signierschlüssel oder Datenbank-Zugangsdaten nicht in die Versionsverwaltung auf; ignorieren Sie
.local/ in Ihrem eigenen Projekt.
Projektstruktur
src/
├── config/database.ts
├── controllers/userController.ts
├── middleware/errorHandler.ts
├── middleware/auth.ts
├── models/User.ts
├── routes/userRoutes.ts
├── local/createKeys.ts
├── local/issueToken.ts
├── app.ts
└── index.ts
Datenbankverbindung einrichten
Starten Sie eine temporäre Datenbank mit dem offiziellen PostgreSQL-Image. Docker weist einen verfügbaren Loopback-Port zu. Das folgende Passwort ist nur für dieses lokale Beispiel gedacht; der Datenbankbenutzer des Containers ist ein Superuser und für eine produktiv bereitgestellte Anwendung ungeeignet.
: "${DB_CONTAINER:=stack-api-db-${RANDOM}-${RANDOM}}"
docker run --detach --name "$DB_CONTAINER" \
--publish 127.0.0.1::5432 \
--tmpfs /var/lib/postgresql/data:rw,size=256m \
--env POSTGRES_USER=profiles --env POSTGRES_DB=profiles \
--env POSTGRES_PASSWORD=local-only-password postgres:17.11-bookworm
Prüfen Sie die TCP-Bereitschaft mit 30 Wiederholungen und jeweils einer Sekunde Pause zwischen den Versuchen. Fahren Sie nur fort, wenn dieser Block erfolgreich ausgeführt wurde:
(
for attempt in {1..30}; do
if docker exec "$DB_CONTAINER" pg_isready -h 127.0.0.1 -U profiles -d profiles; then
exit 0
fi
sleep 1
done
printf 'PostgreSQL did not become ready; inspect docker logs.\n' >&2
exit 1
)
Lassen Sie dieses Terminal geöffnet: Die spätere Konfiguration und Bereinigung verwenden
DB_CONTAINER. Die Datenbank bleibt während eines API-Neustarts verfügbar,
aber ihre temporären Daten verschwinden, sobald der Datenbankcontainer stoppt. Dies ist bewusst
eine lokale Übung.
Erstellen Sie src/config/database.ts:
import 'reflect-metadata'
import { DataSource } from 'typeorm'
import { User } from '../models/User.js'
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is required')
export const AppDataSource = new DataSource({
type: 'postgres',
url: process.env.DATABASE_URL,
synchronize: process.env.SCHEMA_SYNC === 'development-only',
logging: false,
entities: [User],
})
Die Importspezifizierer mit .js verweisen auf die ausgegebenen Dateien
in diesem eigenständigen NodeNext-Projekt. Kompilieren Sie mit TypeScript:
Das Type Stripping von Node transformiert die Dekoratoren von
TypeORM nicht. Verwenden Sie geprüfte TypeORM-Migrationen
für produktiv bereitgestellte Datenbanken.
Benutzermodell erstellen
Erstellen Sie src/models/User.ts:
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id!: string
@Column({ type: 'varchar', length: 100 })
name!: string
@Column({ type: 'varchar', length: 254, unique: true })
email!: string
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date
}
Es gibt keine Passwortspalte. Das Erstellen eines Profils legt kein Anmeldekonto an und gewährt
keinen Zugriff.
timestamptz
speichert die Erstellungszeit als absoluten Zeitpunkt, unabhängig von der Zeitzone der
Datenbanksitzung.
Middleware zur Fehlerbehandlung
Erstellen Sie src/middleware/errorHandler.ts. Generieren Sie für jede Anfrage eine neue Anfrage-ID;
vertrauen Sie niemals dem Anfrage-ID-Header eines Clients als Audit-Kennung.
import { randomUUID } from 'node:crypto'
import type { ErrorRequestHandler, RequestHandler } from 'express'
import { QueryFailedError } from 'typeorm'
import { z, ZodError } from 'zod'
export class AppError extends Error {
constructor(public statusCode: number, message: string) {
super(message)
}
}
export const requestId: RequestHandler = (_req, res, next) => {
res.locals.requestId = randomUUID()
res.setHeader('X-Request-ID', res.locals.requestId)
res.setHeader('Cache-Control', 'no-store')
next()
}
export const errorHandler: ErrorRequestHandler = (error, _req, res, next) => {
if (res.headersSent) {
next(error)
return
}
let status = 500
let message = 'Request failed'
if (error instanceof AppError) {
status = error.statusCode
message = error.message
} else if (error instanceof ZodError) {
status = 400
message = 'Invalid request'
} else if (error instanceof QueryFailedError &&
z.object({ code: z.literal('23505') }).safeParse(error.driverError).success) {
status = 409
message = 'Email is already in use'
} else {
const parsed = z.object({
type: z.enum(['entity.parse.failed', 'entity.too.large']),
}).safeParse(error)
if (parsed.success) {
status = parsed.data.type === 'entity.too.large' ? 413 : 400
message = 'Invalid request body'
}
}
console.error(JSON.stringify({ event: 'request_failed', requestId: res.locals.requestId, status }))
res.status(status).json({ error: message, requestId: res.locals.requestId })
}
Erstellen Sie src/middleware/auth.ts. Der konfigurierte öffentliche Schlüssel, der
Aussteller, die Zielgruppe und der Algorithmus sind serverseitige Vorgaben; kein Token-Header
oder Anfrageparameter kann sie ersetzen. jose prüft die Signatur und die Claims mit
diesen Prüfoptionen.
Der Server verlangt sub, exp und
iat und lehnt Token ab, die vor mehr als 15 Minuten ausgestellt wurden,
auch wenn ihr Ablaufzeitpunkt später liegt. Ihr Aussteller für den Produktivbetrieb muss diese
Claims ebenfalls liefern.
import type { RequestHandler } from 'express'
import { importSPKI, jwtVerify } from 'jose'
import { AppError } from './errorHandler.js'
const { JWT_PUBLIC_KEY, JWT_ISSUER, JWT_AUDIENCE } = process.env
if (!JWT_PUBLIC_KEY || !JWT_ISSUER || !JWT_AUDIENCE) {
throw new Error('JWT verification configuration is required')
}
const verificationKey = await importSPKI(JWT_PUBLIC_KEY, 'RS256')
export function authorize(scope: string): RequestHandler {
return async (req, _res, next) => {
const match = /^Bearer ([A-Za-z0-9_.-]+)$/.exec(req.headers.authorization ?? '')
if (!match) throw new AppError(401, 'Authentication required')
let payload
try {
const verified = await jwtVerify(match[1], verificationKey, {
algorithms: ['RS256'],
issuer: JWT_ISSUER,
audience: JWT_AUDIENCE,
requiredClaims: ['sub', 'exp', 'iat'],
maxTokenAge: '15m',
})
payload = verified.payload
} catch {
throw new AppError(401, 'Invalid access token')
}
if (typeof payload.scope !== 'string' || !payload.scope.split(' ').includes(scope)) {
throw new AppError(403, 'Permission denied')
}
next()
}
}
Express-Server einrichten
Erstellen Sie src/app.ts:
import express from 'express'
import helmet from 'helmet'
import { errorHandler, requestId } from './middleware/errorHandler.js'
import { userRoutes } from './routes/userRoutes.js'
export const app = express()
app.disable('x-powered-by')
app.use(requestId)
app.use(helmet())
app.use('/api/v1/users', userRoutes)
app.use((_req, res) => res.status(404).json({
error: 'Not found', requestId: res.locals.requestId,
}))
app.use(errorHandler)
Erstellen Sie src/index.ts:
import { once } from 'node:events'
import { z } from 'zod'
import { app } from './app.js'
import { AppDataSource } from './config/database.js'
async function main(): Promise<void> {
const port = z.coerce.number().int().min(1).max(65535).parse(process.env.PORT ?? 3000)
await AppDataSource.initialize()
const server = app.listen(port, '127.0.0.1')
server.requestTimeout = 30_000
server.headersTimeout = 10_000
await once(server, 'listening')
console.log(`Listening on http://127.0.0.1:${port}`)
}
main().catch(async () => {
console.error('API startup failed; check database, JWT settings, and PORT')
if (AppDataSource.isInitialized) await AppDataSource.destroy()
process.exitCode = 1
})
Die Bereitschaftsmeldung erscheint erst, nachdem die Verbindung zu PostgreSQL hergestellt und
der HTTP-Port gebunden wurde. Falls der Start fehlschlägt, prüfen Sie den Datenbankcontainer,
die Schlüsselkonfiguration und ob ein anderer Prozess bereits PORT
verwendet. Wählen Sie einen freien Port, statt einen unbekannten Prozess zu beenden.
Benutzer-Controller
Erstellen Sie src/controllers/userController.ts. Strikte Schemas lehnen zusätzliche Felder ab,
statt id, password,
role oder Zeitstempel stillschweigend zu akzeptieren.
import type { Request, Response } from 'express'
import { z } from 'zod'
import { AppDataSource } from '../config/database.js'
import { AppError } from '../middleware/errorHandler.js'
import { User } from '../models/User.js'
const profiles = AppDataSource.getRepository(User)
const profileSchema = z.object({
name: z.string().trim().min(1).max(100),
email: z.string().trim().email().max(254).transform((value) => value.toLowerCase()),
}).strict()
const patchSchema = profileSchema.partial().refine((value) => Object.keys(value).length > 0)
const idSchema = z.string().uuid()
export async function getUsers(req: Request, res: Response): Promise<void> {
const query = z.object({
offset: z.coerce.number().int().min(0).max(10000).default(0),
limit: z.coerce.number().int().min(1).max(100).default(20),
}).strict().parse(req.query)
const data = await profiles.find({ skip: query.offset, take: query.limit, order: { id: 'ASC' } })
res.json({ data })
}
export async function getUser(req: Request, res: Response): Promise<void> {
const user = await profiles.findOneBy({ id: idSchema.parse(req.params.id) })
if (!user) throw new AppError(404, 'User not found')
res.json({ data: user })
}
export async function createUser(req: Request, res: Response): Promise<void> {
const { name, email } = profileSchema.parse(req.body)
const user = await profiles.save(profiles.create({ name, email }))
res.location('/api/v1/users/' + user.id).status(201).json({ data: user })
}
export async function updateUser(req: Request, res: Response): Promise<void> {
const id = idSchema.parse(req.params.id)
const fields = patchSchema.parse(req.body)
const result = await profiles.update(id, fields)
if (result.affected === 0) throw new AppError(404, 'User not found')
res.status(204).end()
}
export async function deleteUser(req: Request, res: Response): Promise<void> {
const result = await profiles.delete(idSchema.parse(req.params.id))
if (result.affected === 0) throw new AppError(404, 'User not found')
res.status(204).end()
}
Aktualisierungen und Löschvorgänge geben eine leere 204-Antwort zurück. Doppelte normalisierte E-Mail-Adressen führen zu 409. Lesezugriffe geben nur die öffentlichen Felder des Profilmodells zurück.
Benutzerrouten
Erstellen Sie src/routes/userRoutes.ts. Die Authentifizierung erfolgt vor dem JSON-Parsing.
import { json, Router } from 'express'
import { createUser, deleteUser, getUser, getUsers, updateUser } from '../controllers/userController.js'
import { authorize } from '../middleware/auth.js'
export const userRoutes = Router()
const body = json({ limit: '10kb' })
userRoutes.get('/', authorize('profiles:read'), getUsers)
userRoutes.get('/:id', authorize('profiles:read'), getUser)
userRoutes.post('/', authorize('profiles:write'), body, createUser)
userRoutes.patch('/:id', authorize('profiles:write'), body, updateUser)
userRoutes.delete('/:id', authorize('profiles:write'), deleteUser)
Anwendung ausführen
Erstellen Sie src/local/createKeys.ts. Führen Sie dies einmal pro neuem Projekt aus;
bestehende Schlüssel werden nicht überschrieben:
import { generateKeyPairSync } from 'node:crypto'
import { mkdir, writeFile } from 'node:fs/promises'
const pair = generateKeyPairSync('rsa', { modulusLength: 2048 })
await mkdir('.local', { mode: 0o700 })
await writeFile('.local/private.pem', pair.privateKey.export({ type: 'pkcs8', format: 'pem' }), {
flag: 'wx', mode: 0o600,
})
await writeFile('.local/public.pem', pair.publicKey.export({ type: 'spki', format: 'pem' }), {
flag: 'wx', mode: 0o600,
})
Erstellen Sie src/local/issueToken.ts. Dieser lokale Testaussteller gewährt beide
Administrator-Scopes für fünf Minuten. Er ist ein Kommandozeilen-Hilfsprogramm, keine
API-Anmelderoute:
import { readFile } from 'node:fs/promises'
import { importPKCS8, SignJWT } from 'jose'
const key = await importPKCS8(await readFile('.local/private.pem', 'utf8'), 'RS256')
const token = await new SignJWT({ scope: 'profiles:read profiles:write' })
.setProtectedHeader({ alg: 'RS256' })
.setIssuer('https://local-issuer.example.test')
.setAudience('profile-api')
.setSubject('local-admin')
.setIssuedAt()
.setExpirationTime('5m')
.sign(key)
console.log(token)
Kompilieren Sie den TypeScript-Code und erstellen Sie die Schlüssel.
&& verhindert, dass bei einem Kompilierungsfehler ein älterer Build
ausgeführt wird:
NODE_OPTIONS='' corepack yarn build && NODE_OPTIONS='' node dist/local/createKeys.js
Konfigurieren Sie den Server im selben Terminal, in dem Sie PostgreSQL gestartet haben.
JWT_PUBLIC_KEY enthält den PEM-Text mit echten Zeilenumbrüchen.
SCHEMA_SYNC aktiviert die Tabellenerstellung nur für diese temporäre Datenbank;
lassen Sie die Variable für eine produktiv bereitgestellte, mit Migrationen verwaltete Datenbank
ungesetzt.
DB_ADDRESS="$(docker port "$DB_CONTAINER" 5432/tcp)" &&
LOCAL_PUBLIC_KEY="$(cat .local/public.pem)" &&
export DATABASE_URL="postgresql://profiles:local-only-password@${DB_ADDRESS}/profiles" \
JWT_PUBLIC_KEY="$LOCAL_PUBLIC_KEY" \
JWT_ISSUER='https://local-issuer.example.test' JWT_AUDIENCE='profile-api' \
SCHEMA_SYNC=development-only PORT="${PORT:-3000}"
Starten Sie die API im Vordergrund. Warten Sie auf Listening on http://127.0.0.1:3000 beziehungsweise
den von Ihnen gewählten Port:
NODE_OPTIONS='' corepack yarn start
API testen
Öffnen Sie ein zweites Terminal in stack-api. Setzen Sie dort ebenfalls
PORT, falls Sie einen anderen Port gewählt haben.
Führen Sie diese Schritte im selben zweiten Terminal aus, damit das Token und die zurückgegebene
UUID verfügbar bleiben. Halten Sie Token aus Logs und dem Frontend-Quellcode fern. Um ein
abgelaufenes Token zu erneuern, wiederholen Sie den Token-Befehl.
BASE_URL="http://127.0.0.1:${PORT:-3000}"
NEXT_ACCESS_TOKEN="$(NODE_OPTIONS='' node dist/local/issueToken.js)" && ACCESS_TOKEN="$NEXT_ACCESS_TOKEN"
Erstellen Sie das Profil und erfassen Sie die zurückgegebene UUID erst nach einer erfolgreichen Antwort. Falls die Anfrage fehlschlägt, fahren Sie nicht mit dem nächsten Schritt fort. Ein 409 bedeutet, dass die E-Mail-Adresse bereits vorhanden ist. Verwenden Sie eine andere E-Mail-Adresse oder lesen Sie das bestehende Profil, statt einen möglicherweise bereits festgeschriebenen POST blind zu wiederholen.
CREATED="$(curl -fsS -X POST "$BASE_URL/api/v1/users" \
-H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"John Doe","email":"john@example.com"}')" &&
NEXT_PROFILE_ID="$(printf '%s' "$CREATED" | NODE_OPTIONS='' node --input-type=module -e '
import { z } from "zod"
let text = ""
for await (const chunk of process.stdin) text += chunk
const { data } = z.object({ data: z.object({ id: z.string().uuid() }) }).parse(JSON.parse(text))
console.log(data.id)
')" && PROFILE_ID="$NEXT_PROFILE_ID" && printf '%s\n' "$CREATED"
Die Antwort lautet {"data":{"id":"…","name":"John Doe","email":"john@example.com","createdAt":"…"}},
mit HTTP 201 und einem Header Location für das neue Profil.
Lesen Sie das Profil und die paginierte Liste:
curl -fsS "$BASE_URL/api/v1/users/$PROFILE_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" &&
curl -fsS "$BASE_URL/api/v1/users?limit=20&offset=0" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Aktualisieren Sie den Namen. -i zeigt Status und Header an,
da PATCH keinen Antwortkörper zurückgibt:
curl -fsS -i -X PATCH "$BASE_URL/api/v1/users/$PROFILE_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"John Smith"}'
Erwartet wird HTTP 204. Drücken Sie im ersten Terminal Ctrl+C und wiederholen Sie dann den obigen Startblock. Lassen Sie PostgreSQL weiterlaufen. Wiederholen Sie im zweiten Terminal den folgenden GET-Aufruf; dieselbe UUID, der aktualisierte Name, die E-Mail-Adresse und die Erstellungszeit sollten weiterhin vorhanden sein:
curl -fsS "$BASE_URL/api/v1/users/$PROFILE_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Löschen Sie dieses Profil und beobachten Sie anschließend die 404-Antwort. Der letzte Befehl
verzichtet bewusst auf -f, damit Sie das Fehler-JSON lesen können:
curl -fsS -i -X DELETE "$BASE_URL/api/v1/users/$PROFILE_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" &&
curl -sS -i "$BASE_URL/api/v1/users/$PROFILE_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"
DELETE gibt eine leere 204-Antwort zurück. Der folgende GET-Aufruf gibt 404 mit
error und requestId zurück.
Jede Antwort enthält einen neuen Wert für X-Request-ID; die ID im Fehlerkörper
stimmt mit der im Header überein. Die Authentifizierung erfolgt vor dem Parsen des Anfragekörpers,
und der JSON-Parser begrenzt den decodierten Anfragekörper auf 10 KiB.
API-Dokumentation
Verwenden Sie diese Ergebnisse, wenn Sie diese API in OpenAPI beschreiben:
| Anfrage | Status |
|---|---|
| Gültiges Profil erstellen | 201, mit Location |
| Profil oder paginierte Liste lesen | 200 |
| Bestehendes Profil aktualisieren oder löschen | 204, leerer Antwortkörper |
| Token fehlt, ist abgelaufen, falsch signiert oder hat einen falschen Aussteller bzw. eine falsche Zielgruppe | 401 |
| Gültiges Token ohne den von der Route benötigten Scope | 403 |
| Ungültige UUID, Felder, Paginierung oder fehlerhaftes JSON | 400 |
| Doppelte E-Mail-Adresse nach Entfernen äußerer Leerzeichen und Umwandlung in Kleinbuchstaben | 409 |
| JSON-Anfragekörper über 10 KiB | 413 |
| Unbekannte oder gelöschte Profil-UUID | 404 |
Schreibzugriffe auf Profile akzeptieren nur name und
email. PATCH erfordert mindestens eines dieser Felder. Die Paginierung
verwendet standardmäßig 20 Einträge, mit einem Maximum von 100 und einem Offset zwischen null und
10.000. Dokumentieren Sie die beiden Scopes als Administratorberechtigungen für alle Profile,
einschließlich des Bearer-Token-Schemas und des Anfrage-ID-Headers.
Hinweise zur Bereitstellung
Ersetzen Sie für eine Anwendung mit echten Benutzern den lokalen Signierer durch den Flow Ihres Identitätsanbieters für administrative Zugriffs-Token. Beziehen Sie dessen öffentlichen Schlüssel über eine vertrauenswürdige Konfiguration und legen Sie auf dem Server den passenden Aussteller und die passende Zielgruppe fest. Dieses Beispiel verwendet einen festen Prüfschlüssel; es implementiert weder Schlüsselrotation noch Token-Widerruf.
Stellen Sie die API für Remote-Zugriffe nur hinter HTTPS bereit. Ersetzen Sie die temporäre Superuser-Datenbank durch eine Datenbank mit eingeschränkten Anwendungszugangsdaten und verwenden Sie Migrationen statt Schemasynchronisierung. Das Beenden des Node-Prozesses stoppt PostgreSQL nicht. Wenn Sie fertig sind, stoppen Sie die API und entfernen Sie nur den von Ihnen erstellten Datenbankcontainer, wobei seine temporären Daten verworfen werden:
docker rm --force --volumes "$DB_CONTAINER"
Fazit
Falls diese Profile später Dateianhänge benötigen, bietet die Upload-API von Transloadit einen separaten Dienst zur Annahme und Verarbeitung von Dateien.
