Aplikasi TypeScript dengan tumpukan penuh

Halo, Habr! Saya hadir untuk Anda terjemahan artikel "Aplikasi Full-Stack TypeScript - Bagian 1: Mengembangkan API Backend dengan Nest.js" oleh Ana Ribeiro .


Bagian 1: Mengembangkan API server menggunakan Nest.JS


TL; DR: Ini adalah serangkaian artikel tentang cara membuat aplikasi web TypeScript menggunakan Angular dan Nest.JS. Pada bagian pertama, kita akan menulis API server sederhana menggunakan Nest.JS. Bagian kedua dari seri ini dikhususkan untuk aplikasi front-end menggunakan Angular. Anda dapat menemukan kode final yang dikembangkan dalam artikel ini di repositori GitHub ini .


Apa itu Nest.Js dan mengapa Angular?


Nest.js adalah kerangka kerja untuk membangun aplikasi server web Node.js.


Fitur khasnya adalah ia memecahkan masalah yang tidak dipecahkan oleh kerangka kerja lain: struktur proyek node.js. Jika Anda pernah mengembangkan di bawah node.js, Anda tahu bahwa Anda dapat melakukan banyak hal dengan satu modul (misalnya, middleware Express dapat melakukan segalanya mulai dari otentikasi hingga validasi), yang pada akhirnya dapat menyebabkan "kekacauan" yang tidak didukung. . Seperti yang akan Anda lihat di bawah, nest.js akan membantu kami dengan menyediakan kelas-kelas yang berspesialisasi dalam berbagai masalah.


Nest.js sangat terinspirasi oleh Angular. Sebagai contoh kedua platform menggunakan pelindung untuk memungkinkan atau mencegah akses ke beberapa bagian dari aplikasi Anda, dan kedua platform menyediakan antarmuka CanActivate untuk menerapkan penjaga ini. Namun, penting untuk dicatat bahwa, terlepas dari beberapa konsep yang serupa, kedua struktur tersebut saling independen satu sama lain. Artinya, dalam artikel ini, kami akan membuat API independen untuk front-end kami, yang dapat digunakan dengan kerangka kerja lainnya (Bereaksi, Vue.JS, dan sebagainya).


Aplikasi web untuk pesanan online


Dalam panduan ini, kami akan membuat aplikasi sederhana di mana pengguna dapat memesan di restoran. Ini akan mengimplementasikan logika ini:


  • setiap pengguna dapat melihat menu;
  • hanya pengguna yang sah yang dapat menambahkan barang ke keranjang (melakukan pemesanan)
  • hanya administrator yang dapat menambahkan item menu baru.

Untuk kesederhanaan, kami tidak akan berinteraksi dengan basis data eksternal dan tidak mengimplementasikan fungsionalitas keranjang toko kami.


Membuat struktur file proyek Nest.js


Untuk menginstal Nest.js, kita perlu menginstal Node.js (v.8.9.x atau lebih tinggi) dan NPM. Unduh dan instal Node.js untuk sistem operasi Anda dari situs web resmi (NPM disertakan). Ketika semuanya sudah diinstal, periksa versinya:


node -v # v12.11.1 npm -v # 6.11.3 

Ada berbagai cara untuk membuat proyek dengan Nest.js; mereka dapat ditemukan dalam dokumentasi . Kami akan menggunakan nest-cli . Pasang itu:


npm i -g @nestjs/cli


Selanjutnya, buat proyek kami dengan perintah sederhana:


nest new nest-restaurant-api


dalam prosesnya, sarang akan meminta kami untuk memilih manajer paket: npm atau yarn


Jika semuanya berjalan dengan baik, nest akan membuat struktur file berikut:


 nest-restaurant-api ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ └── main.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── .gitignore ├── .prettierrc ├── nest-cli.json ├── package.json ├── package-lock.json ├── README.md ├── tsconfig.build.json ├── tsconfig.json └── tslint.json 

buka direktori yang dibuat dan mulai server pengembangan:


  #    cd nest-restaurant-api #   npm run start:dev 

Buka browser dan masukkan http://localhost:3000 . Di layar kita akan melihat:


Sebagai bagian dari tutorial ini, kami tidak akan menguji API kami (meskipun Anda harus menulis tes untuk aplikasi yang siap digunakan). Dengan cara ini Anda dapat menghapus direktori test dan menghapus file src/app.controller.spec.ts (yang merupakan test satu). Akibatnya, folder sumber kami berisi file-file berikut:


  • src/app.controller.ts dan src/app.module.ts : file-file ini bertanggung jawab untuk membuat pesan Hello world sepanjang rute / . Karena titik masuk ini tidak penting untuk aplikasi ini kami menghapusnya. Segera Anda akan belajar secara lebih terperinci apa itu pengontrol dan layanan .
  • src/app.module.ts : berisi deskripsi kelas modul tipe, yang bertanggung jawab untuk mendeklarasikan impor, ekspor pengendali dan penyedia ke aplikasi nest.js. Setiap aplikasi memiliki setidaknya satu modul, tetapi Anda dapat membuat lebih dari satu modul untuk aplikasi yang lebih kompleks (lebih banyak di dokumentasi . Aplikasi kami hanya akan berisi satu modul
  • src/main.ts : ini adalah file yang bertanggung jawab untuk memulai server.

Catatan: setelah menghapus src/app.controller.ts dan src/app.module.ts Anda tidak akan dapat memulai aplikasi kami. Jangan khawatir, kami akan segera memperbaikinya.

Buat titik masuk (titik akhir)



API kami akan tersedia pada rute /items . Melalui titik entri ini, pengguna dapat menerima data, dan administrator mengelola menu. Mari kita ciptakan.


Untuk melakukan ini, buat direktori bernama items di dalam src . Semua file yang terkait dengan rute /items akan disimpan di direktori baru ini.


Membuat pengontrol


di nest.js , seperti dalam banyak kerangka kerja lain, pengontrol bertanggung jawab untuk memetakan rute dengan fungsionalitas. Untuk membuat pengontrol di nest.js gunakan dekorator nest.js sebagai berikut: @Controller(${ENDPOINT}) . Selanjutnya, untuk memetakan berbagai metode HTTP , seperti GET dan POST , dekorator @Get , @Post , @Delete , dll. Digunakan.


Dalam kasus kami, kami perlu membuat pengontrol yang mengembalikan hidangan yang tersedia di restoran, dan administrator mana yang akan digunakan untuk mengelola konten menu. Mari kita membuat file bernama items.controller.tc di direktori src/items dengan konten berikut:


  import { Get, Post, Controller } from '@nestjs/common'; @Controller('items') export class ItemsController { @Get() async findAll(): Promise<string[]> { return ['Pizza', 'Coke']; } @Post() async create() { return 'Not yet implemented'; } } 

untuk membuat pengontrol baru kami tersedia di aplikasi kami, daftarkan di modul:


  import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; @Module({ imports: [], controllers: [ItemsController], providers: [], }) export class AppModule {} 

Luncurkan aplikasi kami: npm run start:dev dan buka di browser http: // localhost: 3000 / item , jika Anda melakukan semuanya dengan benar, maka kita akan melihat jawaban atas permintaan kami: ['Pizza', 'Coke'] .


Catatan Penerjemah: untuk membuat pengontrol baru, serta elemen lain dari nest.js : layanan, penyedia, dll., Lebih mudah menggunakan perintah nest generate dari paket nest-cli . Sebagai contoh, untuk membuat controller yang dijelaskan di atas, Anda dapat menggunakan perintah nest generate controller items , sebagai hasilnya sarang akan membuat src/items/items.controller.tc src/items/items.controller.spec.tc dan src/items/items.controller.tc konten berikut:


  import { Get, Post, Controller } from '@nestjs/common'; @Controller('items') export class ItemsController {} 

dan daftarkan di app.molule.tc


Menambahkan Layanan


Sekarang, ketika mengakses /items aplikasi kita mengembalikan array yang sama untuk setiap permintaan, yang tidak bisa kita ubah. Memproses dan menyimpan data bukanlah bisnis pengontrol, untuk tujuan ini layanan dimaksudkan di nest.js
Layanan dalam sarang adalah @Injectable
Nama dekorator berbicara sendiri, menambahkan dekorator ini ke kelas membuatnya dapat disuntikkan ke komponen lain, seperti pengontrol.
Ayo buat layanan kami. Buat file items.service.ts di folder items.service.ts dengan konten berikut:


  import { Injectable } from '@nestjs/common'; @Injectable() export class ItemsService { private readonly items: string[] = ['Pizza', 'Coke']; findAll(): string[] { return this.items; } create(item: string) { this.items.push(item); } } 

dan ubah pengontrol ItemsController (dideklarasikan di items.controller.ts ) untuk menggunakan layanan kami:


  import { Get, Post, Body, Controller } from '@nestjs/common'; import { ItemsService } from './items.service'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<string[]> { return this.itemsService.findAll(); } @Post() async create(@Body() item: string) { this.itemsService.create(item); } } 

Di versi baru controller, kami menerapkan dekorator @Body ke argumen metode create . Argumen ini digunakan untuk secara otomatis mencocokkan data yang dikirimkan melalui req.body ['item'] ke argumen itu sendiri (dalam hal ini, item ).
Pengontrol kami juga menerima turunan dari kelas ItemsService , disuntikkan melalui konstruktor. Mendeklarasikan ItemsService sebagai private readonly membuat instance tidak dapat diubah dan hanya terlihat di dalam kelas.
Dan jangan lupa untuk mendaftarkan layanan kami di app.module.ts :


  import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; import { ItemsService } from './items/items.service'; @Module({ imports: [], controllers: [ItemsController], providers: [ItemsService], }) export class AppModule {} 

Setelah semua perubahan, mari kirim permintaan HTTP POST ke menu:


  curl -X POST -H 'content-type: application/json' -d '{"item": "Salad"}' localhost:3000/items 

Kemudian kami akan memeriksa apakah hidangan baru muncul di menu kami dengan membuat permintaan GET (atau dengan membuka http: // localhost: 3000 / item di browser)


  curl localhost:3000/items 

Membuat Rute Keranjang Belanja


Sekarang kami memiliki versi pertama dari titik masuk /items API kami, mari kita terapkan fungsi keranjang belanja. Proses pembuatan fungsi ini tidak jauh berbeda dari API yang sudah dibuat. Oleh karena itu, agar tidak mengacaukan manual, kami akan membuat komponen yang merespons dengan status OK saat mengakses.


Pertama, di folder ./src/shopping-cart/ buat file shoping-cart.controller.ts :


  import { Post, Controller } from '@nestjs/common'; @Controller('shopping-cart') export class ShoppingCartController { @Post() async addItem() { return 'This is a fake service :D'; } } 

Daftarkan pengontrol ini di modul kami ( app.module.ts ):


  import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; import { ShoppingCartController } from './shopping-cart/shopping-cart.controller'; import { ItemsService } from './items/items.service'; @Module({ imports: [], controllers: [ItemsController, ShoppingCartController], providers: [ItemsService], }) export class AppModule {} 

Untuk memverifikasi titik entri ini, jalankan perintah berikut, setelah memastikan bahwa aplikasi sedang berjalan:


  curl -X POST localhost:3000/shopping-cart 

Menambahkan Skrip Antarmuka untuk Item


Kembali ke layanan items kami. Sekarang kami hanya menyimpan nama hidangan, tetapi ini jelas tidak cukup, dan, tentu saja, kami ingin memiliki lebih banyak informasi (misalnya, biaya hidangan). Saya pikir Anda akan setuju bahwa menyimpan data ini sebagai array string bukan ide yang baik?
Untuk mengatasi masalah ini, kita bisa membuat array objek. Tetapi bagaimana cara menyimpan struktur benda? Di sini antarmuka TypeScript akan membantu kita, di mana kita mendefinisikan struktur objek items . Buat file baru bernama item.interface.ts di folder src/items :


  export interface Items { readonly name: string; readonly price: number; } 

Kemudian items.service.ts file items.service.ts :


 import { Injectable } from '@nestjs/common'; import { Item } from './item.interface'; @Injectable() export class ItemsService { private readonly items: Item[] = []; findAll(): Item[] { return this.items; } create(item: Item) { this.items.push(item); } } 

Dan juga di items.controller.ts :


 import { Get, Post, Body, Controller } from '@nestjs/common'; import { ItemsService } from './items.service'; import { Item } from './item.interface'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<Item[]> { return this.itemsService.findAll(); } @Post() async create(@Body() item: Item) { this.itemsService.create(item); } } 

Validasi input di Nest.js


Terlepas dari kenyataan bahwa kami menentukan struktur objek item , aplikasi kami tidak akan mengembalikan kesalahan jika kami mengirim permintaan POST yang tidak valid (semua jenis data tidak didefinisikan dalam antarmuka). Misalnya, untuk permintaan seperti itu:


  curl -H 'Content-Type: application/json' -d '{ "name": 3, "price": "any" }' http://localhost:3000/items 

server harus merespons dengan status 400 (permintaan buruk), tetapi sebaliknya, aplikasi kita akan merespons dengan status 200 (OK).


Untuk mengatasi masalah ini, buat DTO (Obyek Transfer Data) dan komponen Pipa (saluran).


DTO adalah objek yang mendefinisikan bagaimana data harus ditransfer antar proses. Kami menjelaskan DTO di file src/items/create-item.dto.ts :


  import { IsString, IsInt } from 'class-validator'; export class CreateItemDto { @IsString() readonly name: string; @IsInt() readonly price: number; } 

Pipa di Nest.js adalah komponen yang digunakan untuk validasi. Untuk API kami, buat saluran untuk memeriksa apakah data yang dikirim ke metode cocok dengan DTO. Satu saluran dapat digunakan oleh pengontrol yang berbeda, jadi buat direktori src/common/ dengan file validation.pipe.ts :


  import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform, } from '@nestjs/common'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; @Injectable() export class ValidationPipe implements PipeTransform<any> { async transform(value, metadata: ArgumentMetadata) { const { metatype } = metadata; if (!metatype || !this.toValidate(metatype)) { return value; } const object = plainToClass(metatype, value); const errors = await validate(object); if (errors.length > 0) { throw new BadRequestException('Validation failed'); } return value; } private toValidate(metatype): boolean { const types = [String, Boolean, Number, Array, Object]; return !types.find(type => metatype === type); } } 

Catatan: Kita perlu menginstal dua modul: class-validator dan class-transformer . Untuk melakukan ini, jalankan npm install class-validator class-transformer di konsol dan restart server.

Menyesuaikan items.controller.ts untuk digunakan dengan pipa dan DTO baru kami:


  import { Get, Post, Body, Controller, UsePipes } from '@nestjs/common'; import { CreateItemDto } from './create-item.dto'; import { ItemsService } from './items.service'; import { Item } from './item.interface'; import { ValidationPipe } from '../common/validation.pipe'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<Item[]> { return this.itemsService.findAll(); } @Post() @UsePipes(new ValidationPipe()) async create(@Body() createItemDto: CreateItemDto) { this.itemsService.create(createItemDto); } } 

Mari kita periksa lagi kode kita, sekarang entri /items hanya menerima data jika didefinisikan dalam DTO. Sebagai contoh:


  curl -H 'Content-Type: application/json' -d '{ "name": "Salad", "price": 3 }' http://localhost:3000/items 

Rekatkan dalam data yang tidak valid (data yang tidak dapat diverifikasi di ValidationPipe ), akibatnya kami mendapatkan jawabannya:


  {"statusCode":400,"error":"Bad Request","message":"Validation failed"} 

Membuat Middleware

Menurut halaman panduan mulai cepat Auth0 , cara yang disarankan untuk memverifikasi token JWT yang dikeluarkan oleh Auth0 adalah dengan menggunakan Express middleware yang disediakan oleh express-jwt . Middleware ini mengotomatisasi sebagian besar pekerjaan.


Mari kita membuat file authentication.middleware.ts di dalam direktori src / common dengan kode berikut:


  import { NestMiddleware } from '@nestjs/common'; import * as jwt from 'express-jwt'; import { expressJwtSecret } from 'jwks-rsa'; export class AuthenticationMiddleware implements NestMiddleware { use(req, res, next) { jwt({ secret: expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: 'https://${DOMAIN}/.well-known/jwks.json', }), audience: 'http://localhost:3000', issuer: 'https://${DOMAIN}/', algorithm: 'RS256', })(req, res, err => { if (err) { const status = err.status || 500; const message = err.message || 'Sorry, we were unable to process your request.'; return res.status(status).send({ message, }); } next(); }); }; } 

Ganti ${DOMAIN} dengan nilai domain dari pengaturan aplikasi Auth0


Catatan Penerjemah: dalam aplikasi nyata, keluarkan DOMAIN menjadi konstanta, dan tetapkan nilainya melalui env (lingkungan virtual)

Instal jwks-rsa express-jwt dan jwks-rsa :


  npm install express-jwt jwks-rsa 

Kita perlu menghubungkan middleware (handler) yang dibuat ke aplikasi kita. Untuk melakukan ini, dalam file ./src/app.module.ts :


  import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; import { AuthenticationMiddleware } from './common/authentication.middleware'; import { ItemsController } from './items/items.controller'; import { ShoppingCartController } from './shopping-cart/shopping-cart.controller'; import { ItemsService } from './items/items.service'; @Module({ imports: [], controllers: [ItemsController, ShoppingCartController], providers: [ItemsService], }) export class AppModule { public configure(consumer: MiddlewareConsumer) { consumer .apply(AuthenticationMiddleware) .forRoutes( { path: '/items', method: RequestMethod.POST }, { path: '/shopping-cart', method: RequestMethod.POST }, ); } } 

Kode di atas mengatakan bahwa permintaan POST ke rute /items dan /shopping-cart dilindungi oleh middleware Express , yang memeriksa token akses dalam permintaan.


Mulai ulang server pengembangan ( npm run start:dev ) dan panggil API Nest.js:


  #     curl -X POST http://localhost:3000/shopping-cart #      TOKEN="eyJ0eXAiO...Mh0dpeNpg" # and issue a POST request with it curl -X POST -H 'authorization: Bearer '$TOKEN http://localhost:3000/shopping-cart 

Manajemen peran dengan Auth0

Saat ini, setiap pengguna dengan token terverifikasi dapat memposting item di API kami. Namun, kami ingin hanya pengguna dengan hak administrator yang dapat melakukan ini. Untuk mengimplementasikan fungsi ini, kami menggunakan aturan (aturan) Auth0 .


Jadi, buka panel kontrol Auth0, di bagian Aturan . Di sana, klik tombol + CREATE RULE dan pilih "Setel peran ke pengguna" sebagai model aturan.



Setelah melakukan ini, kami mendapatkan file JavaScript dengan templat aturan yang menambahkan peran administrator ke setiap pengguna yang memiliki email milik domain tertentu. Mari kita ubah beberapa detail dalam template ini untuk mendapatkan contoh fungsional. Untuk aplikasi kami, kami hanya akan memberikan administrator akses ke alamat email kami sendiri. Kami juga perlu mengubah lokasi untuk menyimpan informasi status administrator.


Saat ini, informasi ini disimpan dalam token identifikasi (digunakan untuk memberikan informasi tentang pengguna), tetapi token akses harus digunakan untuk mengakses sumber daya di API. Kode setelah perubahan akan terlihat seperti ini:


  function (user, context, callback) { user.app_metadata = user.app_metadata || {}; if (user.email && user.email === '${YOUR_EMAIL}') { user.app_metadata.roles = ['admin']; } else { user.app_metadata.roles = ['user']; } auth0.users .updateAppMetadata(user.user_id, user.app_metadata) .then(function() { context.accessToken['http://localhost:3000/roles'] = user.app_metadata.roles; callback(null, user, context); }) .catch(function(err) { callback(err); }); } 

Catatan: ganti ${YOUR_EMAIL} dengan alamat email Anda. Penting untuk dicatat bahwa, sebagai suatu peraturan, ketika Anda berurusan dengan email dalam aturan Auth0, sangat ideal untuk memaksakan verifikasi email . Dalam hal ini, ini tidak diperlukan karena kami menggunakan alamat email kami sendiri.

Catatan Penerjemah: fragmen kode di atas dimasukkan di browser pada halaman konfigurasi aturan Auth0

Untuk memeriksa apakah token yang diteruskan ke API kami adalah token administrator, kita perlu membuat guard Nest.js. Di folder src/common , buat file admin.guard.ts


  import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; @Injectable() export class AdminGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const user = context.getArgs()[0].user['http://localhost:3000/roles'] || ''; return user.indexOf('admin') > -1; } } 

Sekarang, jika kita mengulangi proses login yang dijelaskan di atas dan menggunakan alamat email yang ditentukan dalam aturan, kita akan mendapatkan access_token baru. Untuk memeriksa konten access_token ini, salin dan tempel token ke dalam bidang yang Encoded dari situs https://jwt.io/ . Kita akan melihat bahwa bagian payload token ini berisi larik berikut:


  "http://localhost:3000/roles": [ "admin" ] 

Jika token kami benar-benar menyertakan informasi ini, kami melanjutkan integrasi dengan Auth0. Jadi, buka items.controller.ts dan tambahkan penjaga baru di sana:


  import { Get, Post, Body, Controller, UsePipes, UseGuards, } from '@nestjs/common'; import { CreateItemDto } from './create-item.dto'; import { ItemsService } from './items.service'; import { Item } from './item.interface'; import { ValidationPipe } from '../common/validation.pipe'; import { AdminGuard } from '../common/admin.guard'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<Item[]> { return this.itemsService.findAll(); } @Post() @UseGuards(new AdminGuard()) @UsePipes(new ValidationPipe()) async create(@Body() createItemDto: CreateItemDto) { this.itemsService.create(createItemDto); } } 

Sekarang, dengan token baru kami, kami dapat menambahkan item baru melalui API kami:


  #    npm run start:dev #  POST       curl -X POST -H 'Content-Type: application/json' \ -H 'authorization: Bearer '$TOKEN -d '{ "name": "Salad", "price": 3 }' http://localhost:3000/items 

Catatan Penerjemah: untuk verifikasi, Anda dapat melihat apa yang ada dalam item:
 curl -X GET http://localhost:3000/items 


Ringkasan


Selamat! Kami baru saja selesai membangun API Nest.JS kami dan sekarang kami dapat fokus mengembangkan bagian depan aplikasi kami! Pastikan untuk memeriksa bagian kedua dari seri ini: Aplikasi Full-Stack TypeScript - Bagian 2: Mengembangkan Frontend Angular Apps.


Catatan Penerjemah: Terjemahan bagian kedua dalam proses

Untuk meringkas, dalam artikel ini kami menggunakan berbagai fitur Nest.js dan TypeScript: modul, pengontrol, layanan, antarmuka, pipa, middleware, dan penjaga untuk membuat API Saya harap Anda memiliki pengalaman yang baik dan siap untuk terus mengembangkan aplikasi kami. Jika ada sesuatu yang tidak jelas bagi Anda, maka dokumentasi resmi nest.js adalah sumber yang bagus dengan jawaban

Source: https://habr.com/ru/post/id471348/


All Articles