Piszemy kontrolery w NodeJS jeszcze szybciej i łatwiej dzięki routing-controllers

Za oknem gorąco jak w piekle, ledwo doczołgałam się do komputera :) Ostatnio update'owałam wersję ghosta, umożliwi mi to implementację wielu fajnych feature'ów, poprawiłam także trochę szablon :) Jeśli chcecie się wypowiedzieć to zapraszam serdecznie!

Od paru tygodniu wciąż pracuję nad moją wiedzą z node.js, ogarniam fajne rzeczy i mam dla was prawdziwe szałową bibliotekę :)

Standardowy sposób

Pamiętacie wpis z października? Proste RESTowe API - link

Oto jak rejestrowaliśmy kontroler, a właściwie każdą jego metodę osobno:

app.get('/projects', projectController.getProjects);

A w ten sposób wyglądał sam kontroler:

src/controllers/project
import { Request, Response } from 'express';
import * as mongoose from 'mongoose';
import { ProjectModel } from '../schemas/projectSchema';

export let getProjects = (req: Request, res: Response) => {
    ProjectModel.find({}, (err, project) => {
        if (err) {
            res.send(err);
        }
        res.json(project);
    });
};

Jak zauważyliście poprzednio nie korzystałam z klas, ale dodanie ich nie zmieni tego, że wciąż będzie się powtarzało mnóstwo rzeczy takich jak res.json() czy typowanie requestu i response'a. Nie wspominam nawet o pobieraniu danych z requestu.

To wszystko są pojedyncze linijki, ale nie da się tego jakoś uprościć? Nadać temu czytelności? Da się!

Biblioteka routing-controller

Lekko przypominająca ASP.NET MVC, wspierająca TypeScript i bardzo przemyślana biblioteka to wszystko co czasami potrzebujesz i dzisiaj chce wam opowiedzieć o....  routing-controller.

Napisana pod express.js i koa.js operuje na dobrze przemyślanych dekoratorach, które niesamowicie zwiększają czytelność kodu.

Zacznijmy!

npm i --save express reflect-metadata routing-controllers

W tsconfig.json musimy ustawić sobie:

"emitDecoratorMetadata":true, 
"experimentalDecorators":true, 

i zaraz po konstrukcji express'a musimy zarejestrować wszystkie kontrolery:

import express from "express";
import "reflect-metadata";
import { useExpressServer } from "routing-controllers";

const app = express();
useExpressServer(app, {
    controllers: [ExampleController, StaticController] 
});

app.listen( port, () => {
    console.log( `server started at http://localhost:${ port }` );
} );

Można jeszcze skorzystać z funkcji do tworzenia aplikacji oraz/lub pobierania kontrolerów z konkretnego folderu/folderów.

useExpressServer(app, {
  controllers: [__dirname + "/controllers/*.js"]
});

Najsłodszy kontroler jaki da się napisać

Najpierw spójrzcie na to:

// (1) Pobieranie dekoratorów z biblioteki
import { JsonController, Body, Get, Post, HttpError } from "routing-controllers";

// (2) Dekorator, który 'rejestruje' klasę jako routing, ustawia content-type oraz zmienia format danych na JSON
@JsonController()
export class ExampleController {
  @Get("info")
  public async getInfo(@Body() form: GenericObject) {
    const data = {
       name: 'Kamila',
       site: 'solutionchaser.com'
    };
    return data;
  }
}

Najpierw pobieramy dekoratory z biblioteki, by ich użyć (1). Pierwsze co robimy to przypisujemy dekorator do klasy (2). Może to być  @JsonController() albo  @Controller(). JsonController jest rozszerzeniem Controllera, który zawsze zwraca określony typ odpowiedzi oraz format danych.

Możemy też podać podstawową ścieżkę dla całego kontrolera, domyślna to pusty string:

@Controller("/users")

Jest to bardzo popularne przy tworzeniu klasycznych, restowych api :)

Dekoratory dla metod

Na początek wystawmy publiczną metodą jako end-point. Gdy chcemy ustawić setInfo jako metodę HTTP w której dane przyjdą POST'em to użyjemy dekoratora @Post.

  @Post("/info")
  public async setInfo() {
    const promiseFromDatabase = orm.getInfo();
    return promiseFromDatabase;
  }

Argumentem tego dekoratora jest ścieżka po której będziemy wymieniać dane z serwerem. Jeśli klasa ma zdefiniowaną ścieżkę, to następuje konkatenacja.

Pobieranie parametrów

Kolejnymi ułatwieniami są dekoratory parametrów w metodach, które ułatwiają dogrzebywanie się do interesujących nas danych i czynią metody przejrzystymi.

@Body  pozwala się dostać do danych "ciała" wysłanych w poście, a  @Param do tych w urla. @QueryParam pozwala dostać się do konkretnego parametru z query, a jeśli chcemy wszystkie to istnieje liczba mnoga tego dekoratora @QueryParams.

Można korzystać również z @BodyParam, @HeaderParam, @CookieParam, @Session (chodzi o sesję w expressie/koa), @State oraz @UploadedFile, który pomaga uporać się z plikami :)

  @Post("/info/:id")
  public async setInfo(@Param id: string, @QueryParam("page") limit: number, @Body() info: Info) {
    const promiseFromDatabase = orm.setInfo(info);
    return promiseFromDatabase;
  }

Istnieje sposób na wymuszenie parametru bez jakiegokolwiek sprawdzania w kodzie za pomocą required @Body({ required: true }) user: any . Jeśli metoda nie otrzyma tego parametru, nie wykona się i zwróci błąd.

Zwrócenie promise'a w wystawionej metodzie spowoduje, że biblioteka routing-controller zawsze poczeka na jego wykonanie i zwróci odpowiedź. Zaraz dokładniej opowiem o interceptorach, więc czytajcie dalej :)

Ponieważ większość operacji jest asynchroniczna jest to szalenie wygodne :)

Pozostałe dekoratory

Jeśli chcecie nadpisać content-type, nie musicie już modyfikować odpowiedzi, @ContentType("text/cvs") przed rozpoczęciem metody to jedyne czego potrzebujecie.

Chcecie zrobić przekierowanie? To bardzo proste: @Redirect("https://solutionchaser.com") . Tu musicie jednak pamiętać, że cokolwiek zwrócicie w metodzie będzie miało pierwszeństwo przed dekoratorem.

Chcecie, żeby dany end-point zawsze zwracał 404? Nie pytam dlaczego :) Proszę: @HttpCode(404) .

Zawsze renderował statyczny plik HTML: @Render("index.html") lub ustawiał dowolny header @Header("Access-Control-Allow-Origin", "*").

Biblioteka pozwala również tworzenie swoich błędów HTTP:

import { HttpError } from "routing-controllers";

class MyNewError extends HttpError {
    constructor() {
        super(500, "Ups, nie udało się nam");
    }
}

A potem wykorzystanie ich w kontrolerze:

  @Post("/api/form")
  public async setInfo (@Body() info: Info) {
    const validation = check(form);

    if (validation !== true) {
      return new HttpError(500, 'Źle wypełniony formularz');
    }

    return getRepository(Info).save(newInfo);
  };

Middleware z biblioteką routing-controller

Jeszcze jednym udogodnieniem jest prostsze i czytelniejsze podpinanie middleware'ów. Istnieją dwa predefiniowane znane z express.js: @UseBefore i @UseAfter oraz ogólny @Middleware. Tyczy się to tylko middleware'u podpiętego pod konkretne metody oraz klasy, globalne nadal definiujemy przy konstrukcji express.js.

Spróbujmy znaleźć jakiś fajny przykład:

import { JsonController, Get, UseBefore, Middleware } from "routing-controllers";
import { myMiddleware, anotherMiddleware } from 'middleware/myMiddleware';

@JsonController()
@Middleware(anotherMiddleware())
export class ExampleController {

    @Get("/information")
    @UseBefore(myMiddleware())
    public getImportantInformation() {
        // ...
    }
}

To oczywiście nie wyczerpuje tematu, ale tworzenie middleware'ów zasługuje na osobny post :)

Biblioteka obsługuje także interceptory, czyli funkcje modyfikujące zwracane dane. Jednym z nich, domyślnym dla całej biblioteki, jest właśnie obsługa promise'a w odpowiedzi :)

import { Get,  UseInterceptor } from "routing-controllers";
import { functionWhichCutArrayToFirstElement } from 'utils';

@JsonController()
export class ExampleController {
    @Get("/firstElement")
    @UseInterceptor(functionWhichCutArrayToFirstElement)
    getOne(@Param("id") id: number) {
        return ['kot', 'pies', 'papuga']
}

Nie będę pisała całego interceptora, ale załóżmy, że functionWhichCutArrayToFirstElement jak sama nazwa wskazuje wyrzuca wszystkie elementy z tablicy poza pierwszym. Także odpowiedzią https://localhost:8080/firstElement będzie ['kot'];

Podsumowanie

Podoba się Wam? To poczekajcie na resztę, bo część chciałabym opisać dużo dokładniej i z praktycznym użyciem w projekcie.  Możecie spodziewać się (SPOILER ALERT ;) pokrewnych wpisów o tworzeniu dekoratorów, interceptorów oraz autoryzacji przy pomocy routing controller.

Moim zdaniem to świetna biblioteka, która bardzo poprawia czytelność kodu oraz ułatwia pisanie go. Jeśli znacie coś podobnego, to piszcie śmiało!

Dodatkowo zapraszam do komentowania wyglądu bloga i funkcjonalności, trochę poprawiłam szablon i jestem ciekawa opinii :)

Kamila

Dziękuję za poświęcony czas, będzie mi bardzo miło jak zostawisz komentarz :)