El primer lugar de trabajo o cómo comenzar a desarrollar API en Node.js

Introduccion


En este artículo me gustaría compartir mis emociones y habilidades adquiridas en el desarrollo de la primera API REST en Node.js usando TypeScript, como dicen, desde cero. La historia es bastante banal: “Me gradué de la universidad, recibí un diploma. ¿A dónde ir a trabajar? Como me habrás adivinado, el problema no se libró, a pesar de que no tuve que pensar demasiado. El desarrollador (un graduado de la misma especialidad) solicitó una pasantía. Creo que esta es una práctica bastante común y hay muchas historias similares. Sin pensarlo dos veces, decidí probar suerte y fui ...

imagen

Primer dia Introduciendo Node.js


Llegué al desarrollo del back-end. Esta empresa de TI utiliza la plataforma Node.js , con la que no estaba completamente familiarizado. Me adelanté un poco, olvidando decirle al lector que nunca había desarrollado nada en JavaScript (excepto un par de scripts con código de copia). En general, entendí el algoritmo de trabajo y la arquitectura de las aplicaciones web, ya que desarrollé CRUD en Java, Python y Clojure, pero esto no fue suficiente. Por lo tanto, el primer día que me dediqué por completo al estudio de Node.js, este screencast realmente ayudó.

Mientras estudiaba el framework web Express , el administrador de paquetes npm , así como archivos como package.json y tsconfig.json, mi cabeza simplemente dio la vuelta a la cantidad de información. Otra lección es que dominar todo el material al mismo tiempo es una tarea casi imposible. Al final del día, ¡todavía logré configurar el entorno y pude ejecutar el servidor web express! Pero era demasiado temprano para alegrarse, porque se fue a su casa con una gran sensación de malentendido. La sensación de que me estaba ahogando en el vasto mundo de JS no me dejó ni un minuto, por lo que fue necesario reiniciar.

Segundo dia Introduciendo TypeScript


Ese mismo reinicio siguió ese mismo día. En este punto, reconocí completamente mi problema, pasaremos a un poco más abajo. Sabiendo que no era necesario escribir en JavaScipt puro, la capacitación de Node.js fluyó sin problemas al lenguaje TypeScript, es decir, sus características y sintaxis. Aquí vi los tipos tan esperados, sin los cuales la programación fue literalmente hace 2 días, no en lenguajes de programación funcionales. Este fue mi mayor error, lo que me impidió comprender y aprender el código escrito en JavaScript el primer día.

Anteriormente escribió en su mayor parte en lenguajes de programación orientados a objetos como Java, C ++, C #. Al darme cuenta de las posibilidades de TypeScript, me sentí a gusto. Este lenguaje de programación literalmente me inspiró la vida de este entorno complejo, como me pareció en ese momento. Hacia el final del día, configuré completamente el entorno, inicié el servidor (ya en TypeScript), conecté las bibliotecas necesarias, que analizaré a continuación. En pocas palabras: listo para desarrollar la API. Pasamos directamente al desarrollo ...

Desarrollo API


Una explicación del principio del trabajo y otras explicaciones de lo que es la API REST, la dejaremos, porque el foro tiene muchos artículos sobre esto con ejemplos y desarrollo en varios lenguajes de programación.
imagen

La tarea fue la siguiente:

Realiza un servicio con una API REST. Autorización por token portador (/ info, / latencia, / logout). CORS configurado para acceder desde cualquier dominio. DB - MongoDB. Crea un token en cada llamada.

Descripción de la API:

  1. / signin [POST] - solicita portador de token por id y contraseña // recibe datos en json
  2. / signup [POST] - registro de un nuevo usuario: // recibe datos en json
  3. / info [GET]: devuelve el ID de usuario y el tipo de ID, requiere el token emitido por el portador en la autenticación
  4. / latencia [GET]: devuelve un retraso (ping), requiere el token emitido por el portador en la autenticación
  5. / logout [GET]: con el parámetro all: true: elimina todos los tokens de portador de usuario o false: elimina solo el token de portador actual

Noto de inmediato, la tarea parece increíblemente simple para un desarrollador de aplicaciones web. ¡Pero la tarea debe implementarse en un lenguaje de programación, del cual hace 3 días no sabía nada! Incluso para mí, parece completamente transparente en papel y en Python la implementación tomó un poco de tiempo, pero no tenía esa opción. La pila de desarrollo presagió problemas.

Medios de implementación


Entonces, mencioné que el segundo día que ya estudié varias bibliotecas (frameworks), comenzaremos con esto. Para el enrutamiento, elegí los controladores de enrutamiento , guiados por muchas similitudes con los decoradores de Spring Framework (Java). Como ORM, elegí typeorm , aunque trabajando con MongoDB en modo experimental, es suficiente para tal tarea. Usé uuid para generar tokens, las variables se cargan usando dotenv .

Inicio del servidor web


Por lo general, express se usa en su forma pura, pero mencioné el marco de Routing Controllers, que nos permite crear un servidor express de la siguiente manera:

//  Express const app = createExpressServer({ // routePrefix: process.env.SERVER_PREFIX, //  defaults: { nullResultCode: Number(process.env.ERROR_NULL_RESULT_CODE), undefinedResultCode: Number(process.env.ERROR_NULL_UNDEFINED_RESULT_CODE), paramOptions: { required: true } }, //   authorizationChecker: authorizationChecker, // controllers: [UserController] }); //  app.listen(process.env.SERVER_PORT, () => { console.log(process.env.SERVER_MASSAGE); }); 


Como puede ver, no hay nada complicado. De hecho, el marco tiene muchas más funciones, pero no era necesario.
  • routePrefix es solo un prefijo en su url después de la dirección del servidor, por ejemplo: localhost : 3000 / prefix
  • valores predeterminados: nada interesante, solo inicialice los códigos de error
  • autorizaciónChecker: una gran oportunidad para que el marco verifique la autorización del usuario, luego lo consideraremos con más detalle
  • controladores es uno de los principales campos donde especificamos los controladores utilizados en nuestra aplicación


Conexión DB


Anteriormente, ya habíamos lanzado el servidor web, por lo que continuaremos conectándonos a la base de datos MongoDB, habiéndolo implementado previamente en el servidor local. La instalación y configuración se describen en detalle en la documentación oficial . Consideraremos directamente la conexión usando typeorm:

 //  createConnection({ type: 'mongodb', host: process.env.DB_HOST, database: process.env.DB_NAME_DATABASE, entities: [ User ], synchronize: true, logging: false }).catch(error => console.log(error)); 


Todo es bastante simple, debe especificar varios parámetros:

  • tipo - DB
  • host: dirección IP donde implementó la base de datos
  • base de datos: el nombre de la base de datos que se creó anteriormente en mongodb
  • sincronizar: sincronización automática con la base de datos (Nota: era difícil dominar la migración en ese momento)
  • entidades: aquí indicamos las entidades con las que se realiza la sincronización


Ahora conectamos el inicio del servidor y la conexión a una base de datos. Observo que la importación de recursos es diferente de la clásica utilizada en Node.js. Como resultado, obtenemos el siguiente archivo ejecutable, en mi caso main.ts:

 import 'reflect-metadata'; import * as dotenv from 'dotenv'; import { createExpressServer } from 'routing-controllers'; import { createConnection } from 'typeorm'; import { authorizationChecker } from './auth/authorizationChecker'; import { UserController } from './controllers/UserController'; import { User } from './models/User'; dotenv.config(); //  createConnection({ type: 'mongodb', host: process.env.DB_HOST, database: process.env.DB_NAME_DATABASE, entities: [ User ], synchronize: true, logging: false }).catch(error => console.log(error)); //  Express const app = createExpressServer({ // routePrefix: process.env.SERVER_PREFIX, //  defaults: { nullResultCode: Number(process.env.ERROR_NULL_RESULT_CODE), undefinedResultCode: Number(process.env.ERROR_NULL_UNDEFINED_RESULT_CODE), paramOptions: { required: true } }, //   authorizationChecker: authorizationChecker, // controllers: [UserController] }); //  app.listen(process.env.SERVER_PORT, () => { console.log(process.env.SERVER_MASSAGE); }); 

Entidades


Permítame recordarle que la tarea es autenticar y autorizar a los usuarios, respectivamente, necesitamos una entidad: Usuario. ¡Pero eso no es todo, ya que cada usuario tiene un token y no uno! Por lo tanto, es necesario crear una entidad Token.

Usuario

 import { ObjectID } from 'bson'; import { IsEmail, MinLength } from 'class-validator'; import { Column, Entity, ObjectIdColumn } from 'typeorm'; import { Token } from './Token'; //  @Entity() export class User { //  @ObjectIdColumn() id: ObjectID; //Email    @Column() @IsEmail() email: string; //  @Column({ length: 100 }) @MinLength(2) password: string; //  @Column() token: Token; } 

En la tabla Usuario, creamos un campo: una matriz de los mismos tokens para el usuario. También habilitamos calss-validator , ya que es necesario que el usuario inicie sesión por correo electrónico.

Token

 import { Column, Entity } from 'typeorm'; //   @Entity() export class Token { @Column() accessToken: string; @Column() refreshToken: string; @Column() timeKill: number; } 

La base es la siguiente:

imagen

Autorización de usuario


Para la autorización, utilizamos autorizacionesChecker (uno de los parámetros al crear el servidor, ver arriba), por conveniencia, lo ponemos en un archivo separado:

 import { Action, UnauthorizedError } from 'routing-controllers'; import { getMongoRepository } from 'typeorm'; import { User } from '../models/User'; export async function authorizationChecker(action: Action): Promise<boolean> { let token: string; if (action.request.headers.authorization) { //   token = action.request.headers.authorization.split(" ", 2); const repository = getMongoRepository(User); const allUsers = await repository.find(); for (let i = 0; i < allUsers.length; i++) { if (allUsers[i].token.accessToken.toString() === token[1]) { return true; } } } else { throw new UnauthorizedError('This user has not token.'); } return false; } 

Después de la autenticación, cada usuario tiene su propio token, por lo que podemos obtener el token necesario de los encabezados de la respuesta, se ve así: Portador 046a5f60-c55e-11e9-af71-c75526de439e . Ahora podemos verificar si este token existe, después de lo cual la función devuelve información de autorización: verdadero: el usuario está autorizado, falso: el usuario no está autorizado. En la aplicación, podemos usar un decorador muy conveniente en el controlador: @Authorized (). En este punto, se llamará a la función AuthorChecker, que devolverá una respuesta.

La lógica


Para comenzar, me gustaría describir la lógica de negocios, ya que el controlador es una línea de llamadas de método debajo de la clase presentada. Además, en el controlador aceptaremos todos los datos, en nuestro caso serán JSON y Query. Consideraremos los métodos para tareas individuales, y al final formaremos el archivo final, que se llama UserService.ts. Observo que en ese momento simplemente no había suficiente conocimiento para eliminar las dependencias. Si no ha cumplido con el término inyección de dependencia, le recomiendo leer al respecto. En el momento en que uso el marco DI, es decir, uso contenedores, es decir, inyección a través de constructores. Aquí, creo, es un buen artículo para su revisión. Volvemos a la tarea.

  • / signin [POST] : autenticación del usuario registrado. Todo es muy simple y transparente. Solo necesitamos encontrar a este usuario en la base de datos y emitir un nuevo token. Para leer y escribir, se utiliza MongoRepository.

     async userSignin(user: User): Promise<string> { // Mongo repository const repo = getMongoRepository(User); //       let userEmail = await repo.findOne({ email: user.email, password: user.password }); if (userEmail) { //  userEmail = await this.setToken(userEmail); //    repo.save(userEmail); return userEmail.token.accessToken; } return process.env.USER_SERVICE_RESPONSE; } 
  • / signup [POST] - registra un nuevo usuario. Un método muy similar, ya que al principio también estamos buscando un usuario para que no tengamos usuarios registrados con un solo correo electrónico. A continuación, escribimos el nuevo usuario en la base de datos, después de emitir el token.

     async userSignup(newUser: User): Promise<string> { // Mongo repository const repo = getMongoRepository(User); //   email (   2    email) const userRepeat = await repo.findOne({ email: newUser.email }); if (!userRepeat) { //  newUser = await this.setToken(newUser); //   const addUser = getMongoManager(); await addUser.save(newUser); return newUser.token.accessToken; } else { return process.env.USER_SERVICE_RESPONSE; } } 
  • / info [GET] : devuelve la identificación de usuario y el tipo de identificación, requiere el token emitido por el portador en autenticación La imagen también es transparente: primero obtenemos el token actual del usuario de los encabezados de solicitud, luego lo buscamos en la base de datos y determinamos a quién pertenece, y devolvemos el usuario encontrado.

     async getUserInfo(req: express.Request): Promise<User> { // Mongo repository const repository = getMongoRepository(User); //    const user = await this.findUser(req, repository); return user; } private async findUser(req: express.Request, repository: MongoRepository<User>): Promise<User> { if (req.get(process.env.HEADER_AUTH)) { //  const token = req.get(process.env.HEADER_AUTH).split(' ', 2); //    const usersAll = await repository.find(); //  for (let i = 0; i < usersAll.length; i++) { if (usersAll[i].token.accessToken.toString() === token[1]) { return usersAll[i]; } } } } 

  • / latencia [GET] : devuelve un retraso (ping), requiere el token emitido por el portador en la autenticación. Sin embargo, un párrafo del artículo completamente poco interesante. Aquí utilicé solo una biblioteca preparada para verificar el retraso de tcp-ping.

     getLatency(): Promise<IPingResult> { function update(progress: number, total: number): void { console.log(progress, '/', total); } const latency = ping({ address: process.env.PING_ADRESS, attempts: Number(process.env.PING_ATTEMPTS), port: Number(process.env.PING_PORT), timeout: Number(process.env.PING_TIMEOUT) }, update).then(result => { console.log('ping result:', result); return result; }); return latency; } 
  • / logout [GET] : con el parámetro all: true: elimina todos los tokens de portador de usuario o false: elimina solo el token de portador actual. Solo necesitamos encontrar al usuario, verificar el parámetro de consulta y eliminar los tokens. Creo que todo debería estar claro.

     async userLogout(all: boolean, req: express.Request): Promise<void> { // Mongo repository const repository = getMongoRepository(User); //    const user = await this.findUser(req, repository); if (all) { // true    user.token.accessToken = process.env.GET_LOGOUT_TOKEN; user.token.refreshToken = process.env.GET_LOGOUT_TOKEN; //  repository.save(user); } else { // false    user.token.accessToken = process.env.GET_LOGOUT_TOKEN; //  repository.save(user); } } 


Controlador


Muchos no necesitan explicar qué se necesita y cómo se usa el controlador en el patrón MVC, pero aún así diré dos palabras. En resumen, el controlador es el enlace entre el usuario y la aplicación que redirige los datos entre ellos. La lógica se describió completamente anteriormente, cuyos métodos se invocan de acuerdo con las rutas, que consisten en un URI y un servidor ip (ejemplo: localhost: 3000 / signin) . Mencioné anteriormente sobre los decoradores en el controlador: Get , POST , @Authorized y el más importante de ellos es @JsonController. Otra característica muy importante de este marco es que si queremos enviar y recibir JSON, entonces usamos este decorador en lugar de Controller .

 import * as express from 'express'; import { Authorized, Body, Get, Header, JsonController, NotFoundError, Post, QueryParam, Req, UnauthorizedError } from 'routing-controllers'; import { IPingResult } from '@network-utils/tcp-ping'; import { User } from '../models/User'; import { UserService } from '../services/UserService'; //    JSON @JsonController() export class UserController { userService: UserService //  constructor() { this.userService = new UserService(); } //  @Post('/signin') async login(@Body() user: User): Promise<string> { const responseSignin = await this.userService.userSignin(user); if (responseSignin !== process.env.USER_SERVICE_RESPONSE) { return responseSignin; } else { throw new NotFoundError(process.env.POST_SIGNIN_MASSAGE); } } //  @Post('/signup') async registrateUser(@Body() newUser: User): Promise<string> { const responseSignup = await this.userService.userSignup(newUser); if (responseSignup !== process.env.USER_SERVICE_RESPONSE) { return responseSignup; } else { throw new UnauthorizedError(process.env.POST_SIGNUP_MASSAGE); } } //   @Get('/info') @Authorized() async getId(@Req() req: express.Request): Promise<User> { return this.userService.getUserInfo(req); } //   @Authorized() @Get('/latency') getPing(): Promise<IPingResult> { return this.userService.getLatency(); } @Get('/logout') async deleteToken(@QueryParam("all") all: boolean, @Req() req: express.Request): Promise<void> { this.userService.userLogout(all, req); } } 

Conclusión


En este artículo, quería reflejar ya no el componente técnico del código correcto o algo así, sino simplemente compartir el hecho de que una persona puede construir una aplicación web utilizando una base de datos y que contenga al menos algo de lógica desde un cero absoluto en cinco días. Piénselo bien, ningún instrumento le era familiar, recuérdese o simplemente póngalo en mi lugar. En ningún caso es este el caso que dice: "Soy el mejor, nunca puedes hacer eso". Por el contrario, este es un grito del alma de una persona que actualmente está completamente encantada con el mundo de Node.js y comparte esto con usted. Y el hecho de que nada es imposible, ¡solo necesitas tomar y hacer!

Por supuesto, no se puede negar que el autor no sabía nada y se sentó a escribir código por primera vez. No, el conocimiento de OOP, los principios de REST API, ORM y la base de datos estuvieron presentes en cantidades suficientes. Y esto solo puede decir que los medios para lograr el resultado absolutamente no juegan ningún papel y decir en el estilo: "No voy a este trabajo, hay un lenguaje de programación que no aprendí", para mí ahora es solo una manifestación de una persona no de debilidad, sino más bien protección contra un entorno externo desconocido. Pero lo que hay que esconder, el miedo estaba presente conmigo.

Para resumir. Quiero aconsejar a los estudiantes y a las personas que aún no han comenzado su carrera en TI, que no tengan miedo de las herramientas de desarrollo y las tecnologías desconocidas. Los camaradas mayores seguramente lo ayudarán (si tiene suerte al igual que yo), le explicarán en detalle y responderán preguntas, porque cada uno de ellos estaba en esta posición. ¡Pero no olvides que tu deseo es el aspecto más importante!

Enlace al proyecto

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


All Articles