· publié sur dev.to · traduit de l'anglais

Créer une API REST Deno : un début prometteur avec Alosaur et TypeORM

Premiers pas avec Deno sur une API REST, avec Alosaur et TypeORM.

Quand j’ai commencé à écrire des API NodeJS, je suis vite tombé amoureux de NestJS, qui combine ma connaissance d’Angular avec un framework backend bien structuré comme Spring.

Aujourd’hui Deno est presque prêt, et on a déjà quelque chose d’assez proche de Nest dans le monde Deno : ça s’appelle Alosaur !

On dirait qu’il s’est inspiré de NestJS, parce qu’il apporte toutes les bonnes choses :

  • Architecture modulaire
  • Injection de dépendances
  • Schematics Angular
  • Décorateurs
  • Validateur
  • ORM

Installer Deno

Si tu ne connais rien à Deno, un bon point de départ peut être le post Dev.to d’Olivier, ou tu peux l’installer avec les commandes suivantes :

#shell
curl -fsSL https://deno.land/x/install/install.sh | sh 
#powershell
iwr https://deno.land/x/install/install.ps1 -useb | iex
#homebrew
brew install deno

N’oublie pas d’ajouter les variables d’environnement à ton .profile (ou équivalent) et de le recharger si besoin :

export DENO_INSTALL="/home/YOUR_NAME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
source ~/.profile # or equivalent

Tu devrais maintenant pouvoir lancer Deno depuis la console : deno -h

Initialiser le projet

Pour initialiser un nouveau projet, tu n’as vraiment pas besoin de plus que de créer un dossier contenant un fichier TypeScript, par exemple : main.ts

Tu pourras lancer ton projet en tapant deno run main.ts

Intégration VSCode

Dès que tu commences à copier-coller du code depuis la documentation, tu te heurtes vite à des incompatibilités avec l’IDE. Les instructions d’import en sont un bon exemple :

Screen error vscode

Au moment où j’écris cet article, il existe 2 extensions pour Deno. Celle d’Axetroy semble la plus aboutie. Tu dois activer manuellement l’extension Deno dans les paramètres. L’auteur te conseille de le faire uniquement pour ton workspace.

Mise à jour Utilise l’extension officielle Deno

Tu peux créer un .vscode/settings.json contenant ce qui suit :

{
  "deno.unstable": false,
  "[typescript]": {
      "editor.defaultFormatter": "vscode.typescript-language-features",
   },
   "[typescriptreact]": {
       "editor.defaultFormatter": "axetroy.vscode-deno",
   },
   "deno.enable": true,
}

Dans le même temps, dès qu’on va utiliser de belles features comme les décorateurs, il faudra dire à VSCode comment les gérer. La façon habituelle de faire, c’est de créer un tsconfig.json à la racine de ton projet :

{
    "compilerOptions": {
        "plugins": [
            {
                "name": "typescript-deno-plugin"
            }
        ],
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

Alosaur logo

Démarrer avec Alosaur

Comme je le disais, Alosaur est très proche de NestJS. Ce qu’on appelait des modules s’appelle maintenant des Areas. Par exemple, tu pourrais créer une MainArea vide avec le code suivant :

import { App, Area } from 'https://deno.land/x/alosaur/src/mod.ts';
// Declare module
@Area({
})
export class MainArea {}
// Create alosaur application
const app = new App({
    areas: [MainArea],
});
app.listen();

Tu pourras la lancer en utilisant les bons flags : deno run --allow-net --config ./tsconfig.json main.area.ts

  • allow-net sert à donner la permission d’utiliser le réseau
  • config précise la configuration TypeScript, et surtout les décorateurs expérimentaux

Contrôleur Alosaur

Comme pour toute app qui respecte la séparation des responsabilités, un bon point de départ peut être d’implémenter un contrôleur.

import { Controller, Get, Area, App } from 'https://deno.land/x/alosaur/src/mod.ts';
@Controller('/users')
export class UserController {
@Get('')
    getAll() {
        return [{id:1, name:"Jack"}];
    }
}

Comme d’habitude, on utilise le décorateur Controller avec le paramètre “/users” pour déclarer notre classe chargée des requêtes sur l’endpoint “/users”. On déclare aussi une méthode getAll décorée avec le décorateur Get.

Il faut maintenant ajouter notre contrôleur fraîchement créé dans notre MainArea :

@Area({
    controllers: [UserController],
})

N’oublie pas d’importer UserController correctement (avec le .ts)

Si tu lances ton application Deno maintenant, tu devrais pouvoir récupérer des données sur cette URL : http://localhost:8000/users

Service

Une bonne façon d’isoler la logique métier, c’est de créer une couche service. On peut créer un Service avec ceci :

// user.service.ts
export class UserService {
    getAll() {
        return [{ id: 1, name: "Jack" }];
    }
}

On peut maintenant injecter UserService directement dans UserController. Passer par la déclaration dans le constructeur est le moyen le plus court de le faire :

export class UserController {
    constructor(private userService:UserService){}
    @Get('')
    getAll() {
        return this.userService.getAll();
    }
}

microsoft/tsyringe est inclus dans Alosaur. Tsyringe est décrit par Microsoft comme : “A lightweight dependency injection container for TypeScript/JavaScript for constructor injection.”

Les contrôleurs d’Alosaur utilisent l’injection de dépendances par défaut et respectent donc le pattern IoC (inversion of control).

Repository

Ce qu’on veut ensuite, c’est une abstraction pour notre accès aux données. Les repositories sont couramment utilisés pour ça.

// user.repository.ts
export class UserRepository {
    getAll(){
        return [{ id: 1, name: "Jack" }];
    }
}
```

On va utiliser ce repository exactement de la même façon qu'on l'a fait avant avec UserService/UserController.

```ts
export class UserService {
    constructor(private userRepository: UserRepository) {}
    getAll() {
        return this.userRepository.getAll();
    }
}
```

Si tu lances ton app maintenant, tu vas te retrouver avec :

> error: Uncaught TypeInfo not known for class UserService

C'est parce que le mécanisme d'injection de dépendances ne sait pas comment gérer ce __UserRepository__ dans le constructeur. Heureusement, il te suffit de décorer ton __UserService__ avec __AutoInjectable__ pour régler le problème.

AutoInjectable remplace notre constructeur par *"a parameterless constructor that has dependencies auto-resolved"*, c'est pas beau ça ?

```ts
import { AutoInjectable } from "https://deno.land/x/alosaur/src/mod.ts";
import { UserRepository } from '../repository/user.repository.ts';
@AutoInjectable()
export class UserService {
    constructor(private userRepository: UserRepository) {}
    getAll() {
        return this.userRepository.getAll();
    }
}
```

![Typeorm Logo](https://cdn-images-1.medium.com/max/1200/1*5dfDFiYHqrZSgfp47TYN4Q.jpeg)

## Entité TypeORM
Jack est sympa, mais Jack est codé en dur dans notre repository. On veut récupérer les données depuis la base de données.

TypeORM est un puissant micro-framework utilisé pour gérer les bases de données.
*"TypeORMis highly influenced by other ORMs, such as Hibernate, Doctrine and Entity Framework."*

Dans cet article, on va utiliser un [fork](https://github.com/denolib/typeorm) compatible Deno.

La façon la plus simple de démarrer, c'est de créer notre première entité : user.entity.ts

```ts
import { Entity, PrimaryGeneratedColumn, Column } from 'https://denolib.com/denolib/typeorm@v0.2.23-rc3/mod.ts';
@Entity()
export class User{
    @PrimaryGeneratedColumn()
    id!: number;
    
    @Column("varchar", { length: 30 })
    name!: string;
}
```

> Au moment où j'écris cet article, je dois coder en dur la version de TypeORM. Ça pourrait changer dans un futur proche.

- Le décorateur __Entity__ indique que la classe user sera persistée *(dans un contexte de base de données relationnelle, ce sera une table)*
- Le décorateur __PrimaryGeneratedColumn__ décrit notre propriété id comme la valeur primaire auto-générée (id auto-incrémenté).

La clé primaire est obligatoire.

## Configuration de TypeORM

Pour configurer le micro-framework, on doit passer les bons paramètres à la fonction __createConnexion__.

```ts
// init-typeorm.ts
import { createConnection } from 'https://denolib.com/denolib/typeorm@v0.2.23-rc3/mod.ts';
export function initTypeORM() {
return createConnection({
        type: "postgres", // mysql is not currently supported 19/05/2020
        host: "172.17.0.2",
        port: 5432,
        username: "postgres",
        password: "pwd",
        database: "postgres", // default database
        entities: [
            "src/entities/*.ts"
        ],
        synchronize: true,
    });
}
```

Pour être détectée par TypeORM, ton entité user doit se trouver à cet emplacement : __src/entities/user.entity.ts__

Pour lancer la base de données correspondante sur ta machine : [docker hub](https://hub.docker.com/_/postgres)

`docker run --name some-postgres -e POSTGRES_PASSWORD=pwd -d postgres`

> Sur mon Linux, je peux récupérer l'IP du conteneur (172.17.0.2) avec docker inspect.
Sur Mac et Windows, tu devrais pouvoir te connecter à la base en utilisant `--publish=5432:5432`, puis te connecter à la db directement sur localhost.

On doit maintenant appeler initTypeORM depuis notre main area :

```ts
. . . 
await initTypeORM(); // Init before creating the app
const app = new App({
    areas: [MainArea],
});
app.listen();
```

Si tout est bien configuré, on peut maintenant lancer notre app. Le paramètre `"syncronize:true"` va __générer automatiquement les tables manquantes.__

__Pas si vite :__ il va falloir préciser quelques options supplémentaires à Deno :

`deno run --unstable --allow-net --allow-read  --config ./tsconfig.json main.area.ts`

__unstable__ parce qu'à l'heure actuelle, TypeORM utilise certaines features instables de Deno.

__allow-read__ parce que TypeORM scanne le système de fichiers pour trouver dynamiquement les entités.

## Repository custom
La dernière étape pour utiliser toute la puissance de TypeORM, c'est de créer un repository custom. Pour ça, on va décorer notre repository avec __EntityRepository__ et étendre __Repository__ :

```ts
import { EntityRepository } from "https://denolib.com/denolib/typeorm@v0.2.23-rc3/src/decorator/EntityRepository.ts";
import { Repository } from "https://denolib.com/denolib/typeorm@v0.2.23-rc3/src/repository/Repository.ts";
import { User } from '../entities/user.entity.ts';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
}
```

Modifions notre service pour utiliser correctement la méthode *find* de TypeORM :

```ts
import { AutoInjectable } from "https://deno.land/x/alosaur/src/mod.ts";
import { getCustomRepository } from "https://denolib.com/denolib/typeorm@v0.2.23-rc3/src/index.ts";
import { UserRepository } from '../repository/user.repository.ts';
@AutoInjectable()
export class UserService {
    private userRepository: UserRepository;
    constructor() {
        this.userRepository = getCustomRepository(UserRepository);
    }
    getAll() {
        return this.userRepository.find();
    }
}
```

> À ce stade, notre app est lançable sans __AutoInjectable__, mais il y a de grandes chances que sur une vraie app tu aies besoin d'un mécanisme d'injection de dépendances.

Comme tu peux le voir, on utilise maintenant __getCustomRepository__ pour récupérer l'instance de notre repository. Ce n'est malheureusement pas très cohérent avec notre injection de dépendances signée Microsoft, mais je n'ai rien trouvé de mieux pour l'instant.

À ce stade, une requête GET sur [http://localhost:8000/users](http://localhost:8000/users) renvoie un tableau vide.

En revanche, si tu ajoutes manuellement des données dans la table user, tu les récupéreras bien. Tu pourrais aussi déclarer un second endpoint et utiliser TypeORM pour sauvegarder un nouvel utilisateur. (regarde mon repository pour plus d'exemples)

## Conclusion

Deno, succès ou pas ? L'avenir nous le dira, mais j'espère que cet article aidera les gens qui voudraient l'essayer !

Le code source complet de l'article : [https://github.com/hugoblanc/deno-api](https://github.com/hugoblanc/deno-api)