fontcolor_theme
Deepkit HTTP

Eingabe & Ausgabe

Die Eingabe und Ausgabe einer HTTP-Route sind die Daten, die an den Server gesendet werden und die Daten, die an den Client zurückgesendet werden. Dazu gehören die Path-Parameter, Query-Parameter, der Body, Header und die Response selbst. In diesem Kapitel betrachten wir, wie man Daten in einer HTTP-Route liest, deserialisiert, validiert und schreibt.

Input

Alle folgenden Eingabevarianten funktionieren sowohl für die funktionale API als auch für die Controller-API identisch. Sie ermöglichen es, Daten typsicher und entkoppelt aus einer HTTP-Request zu lesen. Das führt nicht nur zu deutlich erhöhter Sicherheit, sondern vereinfacht auch Unit-Tests, da streng genommen nicht einmal ein HTTP-Request-Objekt existieren muss, um die Route zu testen.

Alle Parameter werden automatisch in den definierten TypeScript Type konvertiert (deserialisiert) und validiert. Dies geschieht über Deepkit Runtime Types und dessen Features Serialisierung und Validierung.

Der Einfachheit halber sind unten alle Beispiele mit der funktionalen API gezeigt.

Path-Parameter

Path-Parameter sind Werte, die aus der URL der Route extrahiert werden. Der Type des Werts hängt vom Type am zugehörigen Parameter der Function oder Method ab. Die Konvertierung erfolgt automatisch mit dem Feature Soft Type Conversion.

router.get('/:text', (text: string) => {
    return 'Hello ' + text;
});
$ curl http://localhost:8080/galaxy
Hello galaxy

Wenn ein Path-Parameter als ein anderer Type als string definiert ist, wird er korrekt konvertiert.

router.get('/user/:id', (id: number) => {
    return `${id} ${typeof id}`;
});
$ curl http://localhost:8080/user/23
23 number

Zusätzliche Validierungs-Constraints können ebenfalls auf die Types angewendet werden.

import { Positive } from '@deepkit/type';

router.get('/user/:id', (id: number & Positive) => {
    return `${id} ${typeof id}`;
});

Alle Validation Types aus @deepkit/type können angewendet werden. Mehr dazu siehe HTTP-Validierung.

Die Path-Parameter haben standardmäßig [^]+ als regulären Ausdruck im URL-Matching gesetzt. Die RegExp kann dafür wie folgt angepasst werden:

import { HttpRegExp } from '@deepkit/http';
import { Positive } from '@deepkit/type';

router.get('/user/:id', (id: HttpRegExp<number & Positive, '[0-9]+'>) => {
    return `${id} ${typeof id}`;
});

Dies ist nur in Ausnahmefällen notwendig, da häufig die Types in Kombination mit Validation Types bereits mögliche Werte korrekt einschränken.

Query-Parameter

Query-Parameter sind Werte aus der URL nach dem Zeichen ? und können mit dem Type HttpQuery<T> gelesen werden. Der Name des Parameters entspricht dem Namen des Query-Parameters.

import { HttpQuery } from '@deepkit/http';

router.get('/', (text: HttpQuery<number>) => {
    return `Hello ${text}`;
});
$ curl http://localhost:8080/\?text\=galaxy
Hello galaxy

Query-Parameter werden ebenfalls automatisch deserialisiert und validiert.

import { HttpQuery } from '@deepkit/http';
import { MinLength } from '@deepkit/type';

router.get('/', (text: HttpQuery<string> & MinLength<3>) => {
    return 'Hello ' + text;
}
$ curl http://localhost:8080/\?text\=galaxy
Hello galaxy
$ curl http://localhost:8080/\?text\=ga
error

Alle Validation Types aus @deepkit/type können angewendet werden. Mehr dazu siehe HTTP-Validierung.

Warnung: Parameterwerte sind nicht escaped/sanitized. Deren direkte Rückgabe in einem String in einer Route als HTML eröffnet eine Sicherheitslücke (XSS). Stelle sicher, dass externen Eingaben niemals vertraut wird und filtere/sanitisieren/konvertiere Daten wo notwendig.

Query-Modell

Bei einer großen Anzahl von Query-Parametern kann es schnell unübersichtlich werden. Um wieder Ordnung zu schaffen, kann ein Modell (Class oder Interface) verwendet werden, das alle möglichen Query-Parameter zusammenfasst.

import { HttpQueries } from '@deepkit/http';

class HelloWorldQuery {
    text!: string;
    page: number = 0;
}

router.get('/', (query: HttpQueries<HelloWorldQuery>)
{
    return 'Hello ' + query.text + ' at page ' + query.page;
}
$ curl http://localhost:8080/\?text\=galaxy&page=1
Hello galaxy at page 1

Die Properties im angegebenen Modell können alle TypeScript Types und Validation Types enthalten, die @deepkit/type unterstützt. Siehe die Kapitel Serialisierung und Validierung.

Body

Für HTTP-Methoden, die einen HTTP-Body erlauben, kann ebenfalls ein Body-Modell angegeben werden. Der Body Content-Type des HTTP-Requests muss entweder application/x-www-form-urlencoded, multipart/form-data oder application/json sein, damit Deepkit dies automatisch in JavaScript-Objekte konvertieren kann.

import { HttpBody } from '@deepkit/type';

class HelloWorldBody {
    text!: string;
}

router.post('/', (body: HttpBody<HelloWorldBody>) => {
    return 'Hello ' + body.text;
}

Stream

Manuelles Validierungs-Handling

Um die Validierung des Body-Modells manuell zu übernehmen, kann ein spezieller Type HttpBodyValidation<T> verwendet werden. Er ermöglicht es, auch invalide Body-Daten zu empfangen und sehr spezifisch auf Fehlermeldungen zu reagieren.

import { HttpBodyValidation } from '@deepkit/type';

class HelloWorldBody {
    text!: string;
}

router.post('/', (body: HttpBodyValidation<HelloWorldBody>) => {
    if (!body.valid()) {
        // Houston, wir haben einige Fehler.
        const textError = body.getErrorMessageForPath('text');
        return 'Text is invalid, please fix it. ' + textError;
    }

    return 'Hello ' + body.text;
})

Sobald valid() false zurückgibt, können die Werte im angegebenen Modell in einem fehlerhaften Zustand sein. Das bedeutet, dass die Validierung fehlgeschlagen ist. Wenn HttpBodyValidation nicht verwendet wird und ein fehlerhafter HTTP-Request empfangen wird, würde die Request direkt abgebrochen und der Code in der Function würde niemals ausgeführt werden. Verwende HttpBodyValidation nur, wenn z. B. Fehlermeldungen bezüglich des Bodys in derselben Route manuell verarbeitet werden sollen.

Die Properties im angegebenen Modell können alle TypeScript Types und Validation Types enthalten, die @deepkit/type unterstützt. Siehe die Kapitel Serialisierung und Validierung.

Datei-Upload

Ein spezieller Property Type im Body-Modell kann verwendet werden, um dem Client das Hochladen von Dateien zu erlauben. Es können beliebig viele UploadedFile verwendet werden.

import { UploadedFile, HttpBody } from '@deepkit/http';
import { readFileSync } from 'fs';

class HelloWordBody {
    file!: UploadedFile;
}

router.post('/', (body: HttpBody<HelloWordBody>) => {
    const content = readFileSync(body.file.path);

    return {
        uploadedFile: body.file
    };
})
$ curl http://localhost:8080/ -X POST -H "Content-Type: multipart/form-data" -F "file=@Downloads/23931.png"
{
    "uploadedFile": {
        "size":6430,
        "path":"/var/folders/pn/40jxd3dj0fg957gqv_nhz5dw0000gn/T/upload_dd0c7241133326bf6afddc233e34affa",
        "name":"23931.png",
        "type":"image/png",
        "lastModifiedDate":"2021-06-11T19:19:14.775Z"
    }
}

Standardmäßig speichert der Router alle hochgeladenen Dateien in einem temporären Ordner und entfernt sie, sobald der Code in der Route ausgeführt wurde. Es ist daher notwendig, die Datei im angegebenen Pfad in path zu lesen und an einem dauerhaften Ort (lokale Festplatte, Cloud-Speicher, Datenbank) zu speichern.

Validierung

Validierung in einem HTTP-Server ist eine zwingend notwendige Funktionalität, denn fast immer wird mit nicht vertrauenswürdigen Daten gearbeitet. Je mehr Stellen Daten validiert werden, desto stabiler ist der Server. Validierung in HTTP-Routen kann bequem über Types und Validierungs-Constraints genutzt werden und wird mit einem hochoptimierten Validator aus @deepkit/type geprüft, sodass es diesbezüglich keine Performance-Probleme gibt. Es wird daher dringend empfohlen, diese Validierungsmöglichkeiten zu nutzen. Lieber einmal zu viel als einmal zu wenig.

Alle Inputs wie Path-Parameter, Query-Parameter und Body-Parameter werden automatisch für den angegebenen TypeScript Type validiert. Wenn zusätzliche Constraints über Types von @deepkit/type angegeben sind, werden diese ebenfalls geprüft.

import { HttpQuery, HttpQueries, HttpBody } from '@deepkit/http';
import { MinLength } from '@deepkit/type';

router.get('/:text', (text: string & MinLength<3>) => {
    return 'Hello ' + text;
}

router.get('/', (text: HttpQuery<string> & MinLength<3>) => {
    return 'Hello ' + text;
}

interface MyQuery {
    text: string & MinLength<3>;
}

router.get('/', (query: HttpQueries<MyQuery>) => {
    return 'Hello ' + query.text;
});

router.post('/', (body: HttpBody<MyQuery>) => {
    return 'Hello ' + body.text;
});

Siehe Validierung für weitere Informationen dazu.

Ausgabe

Eine Route kann verschiedene Datenstrukturen zurückgeben. Einige davon werden speziell behandelt, wie Redirects und Templates, und andere, wie einfache Objekte, werden einfach als JSON gesendet.

JSON

Standardmäßig werden normale JavaScript-Werte als JSON mit dem Header applicationjson; charset=utf-8 an den Client zurückgegeben.

router.get('/', () => {
    // wird als application/json gesendet
    return { hello: 'world' }
});

Wenn für die Function oder Method ein expliziter Return Type angegeben ist, werden die Daten mit dem Deepkit JSON Serializer gemäß diesem Type zu JSON serialisiert.

interface ResultType {
    hello: string;
}

router.get('/', (): ResultType => {
    // wird als application/json gesendet und additionalProperty wird entfernt
    return { hello: 'world', additionalProperty: 'value' };
});

HTML

Zum Senden von HTML gibt es zwei Möglichkeiten. Entweder wird das Objekt HtmlResponse oder die Template-Engine mit JSX verwendet.

import { HtmlResponse } from '@deepkit/http';

router.get('/', () => {
    // wird mit Content-Type: text/html gesendet
    return new HtmlResponse('<b>Hello World</b>');
});
router.get('/', () => {
    // wird mit Content-Type: text/html gesendet
    return <b>Hello
    World < /b>;
});

Die Template-Engine-Variante mit JSX hat den Vorteil, dass verwendete Variablen automatisch HTML-escaped werden. Siehe auch Template.

Benutzerdefinierter Content-Type

Neben HTML und JSON ist es auch möglich, Text- oder Binärdaten mit einem bestimmten Content-Type zu senden. Dies geschieht über das Objekt Response.

import { Response } from '@deepkit/http';

router.get('/', () => {
    return new Response('<title>Hello World</title>', 'text/xml');
});

HTTP-Fehler

Durch Werfen verschiedener HTTP-Fehler ist es möglich, die Verarbeitung eines HTTP-Requests sofort zu unterbrechen und den entsprechenden HTTP-Status des Fehlers auszugeben.

import { HttpNotFoundError } from '@deepkit/http';

router.get('/user/:id', async (id: number, database: Database) => {
    const user = await database.query(User).filter({ id }).findOneOrUndefined();
    if (!user) throw new HttpNotFoundError('User not found');
    return user;
});

Standardmäßig werden alle Fehler als JSON an den Client zurückgegeben. Dieses Verhalten kann im Event-System unter dem Event httpWorkflow.onControllerError angepasst werden. Siehe den Abschnitt HTTP-Events.

HttpBadRequestError400
HttpUnauthorizedError401
HttpAccessDeniedError403
HttpNotFoundError404
HttpMethodNotAllowedError405
HttpNotAcceptableError406
HttpTimeoutError408
HttpConflictError409
HttpGoneError410
HttpTooManyRequestsError429
HttpInternalServerError500
HttpNotImplementedError501

Der Fehler HttpAccessDeniedError ist ein Spezialfall. Sobald er geworfen wird, springt der HTTP-Workflow (siehe HTTP-Events) nicht zu controllerError, sondern zu accessDenied.

Eigene HTTP-Fehler können mit createHttpError erstellt und geworfen werden.

export class HttpMyError extends createHttpError(412, 'My Error Message') {
}

Geworfene Fehler in einer Controller-Action werden vom HTTP-Workflow-Event onControllerError behandelt. Die Standardimplementierung besteht darin, eine JSON-Response mit der Fehlermeldung und dem Statuscode zurückzugeben. Dies kann angepasst werden, indem man auf dieses Event lauscht und eine andere Response zurückgibt.

import { httpWorkflow } from '@deepkit/http';

new App()
    .listen(httpWorkflow.onControllerError, (event) => {
        if (event.error instanceof HttpMyError) {
            event.send(new Response('My Error Message', 'text/plain').status(500));
        } else {
            // für alle anderen Fehler eine generische Fehlermeldung zurückgeben
            event.send(new Response('Something went wrong. Sorry about that.', 'text/plain').status(500));
        }
    })
    .listen(httpWorkflow.onAccessDenied, (event) => {
        event.send(new Response('Access denied. Try to login first.', 'text/plain').status(403));
    });

Zusätzliche Header

Um den Header einer HTTP-Response zu ändern, können zusätzliche Methods an den Objekten Response, JSONResponse und HTMLResponse aufgerufen werden.

import { Response } from '@deepkit/http';

router.get('/', () => {
    return new Response('Access Denied', 'text/plain')
        .header('X-Reason', 'unknown')
        .status(403);
});

Redirect

Um eine 301- oder 302-Weiterleitung als Response zurückzugeben, können Redirect.toRoute oder Redirect.toUrl verwendet werden.

import { Redirect } from '@deepkit/http';

router.get({ path: '/', name: 'homepage' }, () => {
    return <b>Hello
    World < /b>;
});

router.get({ path: '/registration/complete' }, () => {
    return Redirect.toRoute('homepage');
});

Die Method Redirect.toRoute verwendet hier den Routenname. Wie man einen Routenname setzt, ist im Abschnitt HTTP-Routennamen zu sehen. Wenn diese referenzierte Route (Query oder Path) Parameter enthält, können diese über das zweite Argument angegeben werden:

router.get({ path: '/user/:id', name: 'user_detail' }, (id: number) => {

});

router.post('/user', (user: HttpBody<User>) => {
    //... store user and redirect to its detail page
    return Redirect.toRoute('user_detail', { id: 23 });
});

Alternativ kann mit Redirect.toUrl auf eine URL weitergeleitet werden.

router.post('/user', (user: HttpBody<User>) => {
    //... store user and redirect to its detail page
    return Redirect.toUrl('/user/' + 23);
});

Standardmäßig verwenden beide eine 302-Weiterleitung. Dies kann über das Argument statusCode angepasst werden.

Resolver

Der Router unterstützt eine Möglichkeit, komplexe Parameter Types zu resolven. Beispiel: Bei einer Route wie /user/:id kann diese id mittels eines Resolvers außerhalb der Route zu einem user-Objekt aufgelöst werden. Das entkoppelt die HTTP-Abstraktion und den Routen-Code weiter und vereinfacht Tests und Modularität zusätzlich.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http, RouteParameterResolverContext, RouteParameterResolver } from '@deepkit/http';

class UserResolver implements RouteParameterResolver {
    constructor(protected database: Database) {
    }

    async resolve(context: RouteParameterResolverContext) {
        if (!context.parameters.id) throw new Error('No :id given');
        return await this.database.getUser(parseInt(context.parameters.id, 10));
    }
}

@http.resolveParameter(User, UserResolver)
class MyWebsite {
    @http.GET('/user/:id')
    getUser(user: User) {
        return 'Hello ' + user.username;
    }
}

new App({
    controllers: [MyWebsite],
    providers: [UserDatabase, UserResolver],
    imports: [new FrameworkModule]
})
    .run();

Der Decorator in @http.resolveParameter gibt an, welche Class mit dem UserResolver aufzulösen ist. Sobald die angegebene Class User als Parameter in der Function oder Method angegeben ist, wird der Resolver verwendet, um sie bereitzustellen.

Wenn @http.resolveParameter an der Class angegeben ist, erhalten alle Methods dieser Class diesen Resolver. Der Decorator kann auch pro Method angewendet werden:

class MyWebsite {
    @http.GET('/user/:id').resolveParameter(User, UserResolver)
    getUser(user: User) {
        return 'Hello ' + user.username;
    }
}

Auch die funktionale API kann verwendet werden:

router.add(
    http.GET('/user/:id').resolveParameter(User, UserResolver),
    (user: User) => {
        return 'Hello ' + user.username;
    }
);

Das User-Objekt muss nicht zwingend von einem Parameter abhängen. Es könnte ebenso von einer Session oder einem HTTP-Header abhängen und nur bereitgestellt werden, wenn der Benutzer eingeloggt ist. In RouteParameterResolverContext stehen viele Informationen über den HTTP-Request zur Verfügung, sodass viele Anwendungsfälle abgebildet werden können.

Grundsätzlich ist es auch möglich, komplexe Parameter Types über den Dependency Injection-Container aus dem http-Scope bereitstellen zu lassen, da diese ebenfalls in der Routen-Function oder -Method verfügbar sind. Dies hat jedoch den Nachteil, dass keine asynchronen Function-Aufrufe verwendet werden können, da der DI-Container durchgängig synchron ist.

English中文 (Chinese)한국어 (Korean)日本語 (Japanese)Deutsch (German)