-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathNotas NodeJSExpress Udemy.txt
1163 lines (943 loc) · 54.4 KB
/
Notas NodeJSExpress Udemy.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
Repositorio:
https://github.com/Jonatandb/CursoUdemyIntroNodeJSExpress
2. Instalado nodejs y visual studio code
https://nodejs.org/es/download/
https://nodejs.org/dist/v12.16.1/node-v12.16.1-x64.msi
Actualizar NodeJS, cambiar versión de node con NPM
http://blog.josmantek.com/nodejs/actualizar-nodejs-cambiar-version-node-npm/
Consultar versión de Node:
node -v
Consultar versión de NPM:
npm -v
Conocer detalles sobre Node instalado:
- Ejecutar la consola de Node
(Buscar en programas instalados el acceso: Node.js)
- Desde la consola ejecutar:
process
- Detalles principales:
title: 'Node.js',
version: 'v10.15.0',
execPath: 'E:\\nodejs\\node.exe',
VSCode:
https://aka.ms/win32-x64-user-stable
Extensiones:
Terminal
Prettier - Code formatter
Git lens
Git history
VSCode-icons
3. Ejecutando una app simple con nodejs
4. Core Modules
Funcionalidades simples o complejas que pueden ser reutilizados en una aplicación nodejs.
Cada módulo tiene su propio contexto, por esto un módulo no puede interferir con otro.
Cada módulo se coloca en un archivo .js diferente en una carpeta separada.
Tipos de módulos:
- Core
Módulos que vienen por defecto cuando instalamos Node JS
Ej:
http Permite ejecutar js del lado del servidor
- Local
- Third Party
5. Local Modules
Tipos de módulos:
- Core
Módulos que vienen por defecto cuando instalamos Node JS
Ej:
fs Permite trabajar con archivos
- Local
Son los módulos que escribimos en nuestra aplicación
- Third Party
6. Importaciones parciales
Cuando desde un módulo mío hago algo como:
module.exports = { funcion1, funcion2 }
Esto se conoce como una EXPORTACIÓN GLOBAL
Para realizar exportaciones parciales, se debe exportar cada función
de la siguiente manera:
module.exports.inf = function info (param) {
Para realicar importaciones parciales, se debe importar cada función
de la siguiente manera:
var { funcion1, funcion2 } = require('./modules/my-log')
Otra manera de hacer una exportación parcial es escribir lo siguiente
solo al final del archivo que tiene las funciones que deseo exportar:
module.exports.info = info // aquí exporto la función "info" (que debe
haber sido declarada más arriba en mi arhcivo), con el nombre "info" (el
cual debe ser el utilizado cuando se haga el require).
* No es obligatorio que el nombre con el que se exporta sea igual al nombre
de la función.
7. Instalando un módulo desde NPM
Tipos de módulos:
- Third Party
Son módulos desarrollados por otras personas
Repositorio con módulos publicos disponibles para instalar con NPM:
https://www.npmjs.com/
Instalación de un módulo con npm:
1º - Para poder instalar un módulo (paquete) con npm, primero debemos inicializar
la carpeta donde estamos trabajando (lo que hará que pase a ser un paquete)
Esto se hace ejecutando:
npm init
Se ingresan los datos solicitados por el asistente para establecer los detalles
del paquete que se está creando.
Esto genera un archivo llamado:
package.json
2º - Se instala el paquete deseado utilizando el comando siguiente:
npm install nombreDelPaquete
Ej:
npm i countries-list
* Es válido utilizar solo la letra "i", en lugar de "install"
Esto agrega en el archivo package.json una nueva sección llamada
"dependencies"
la cual incluye el nombre del paquete externo del cual depende nuestro paquete.
Junto con el nombre de la nueva dependencia, aparece la versión actual de la msima
y a su vez éste número incluye delante el simbolo ^:
"^2.5.1"
Este simbolo le dice a NPM que cuando nuestro paquete sea instalado, se descargue
e instale la dependencia indicada en su versión más actualizada (siempre y cuando
la misma comienza con el 2). Pero esto puede traer un problema, que es que si al momento
de instalarse alguien nuestro paquete, el desarrollador de la dependencia lanzó una
nueva versión que mantiene como primer número el 2, pero que incluye cambios que rompen
compatibilidad con versiones anteriores, lo que va a pasar es que nuestro paquete no va
a funcionar.
Para evitar esto es que entre NODE y NPM se acordó la creación de un nuevo archivo llamado
"package-lock.json", el cual indica que versión expecífica de cada dependencia se debe
instalar cuando se instale nuestro paquete, más allá de que en el package.json las mismas
tengan delante de su número de versión el simbolo ^.
También crea la carpeta "node_modules"
Donde se descargarán los archivos de las dependencias instaladas.
Creación de un script NPM
En el archivo package.json, en la sección "Scripts", se agrega una nueva entrada
con nombre "start" y luego de dos puntos se agrega su definición, ej:
"start": "node index.js"
Para ejecutar este script debo ejecutar:
npm start // o npm run start
Uso del módulo core: url
El módulo url posee un método llamado parse() que recibe la propiedad url del objeto request:
var parsed = url.parse(request.url)
y devuelve un objeto llamado Url con toda la información de la url recibida:
Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?code=AR',
query: 'code=AR',
pathname: '/country',
path: '/country?code=AR',
href: '/country?code=AR'
}
Uso del módulo core: querystring
El módulo querystring posee un método llamado parse que recibe una cadena con el formato
"clave=valor&clave2=valor2":
var query = querystring.parse('code=AR')
y devuelve un objeto que posee propiedades con los nombres de las claves y como valores de
las mismas los valores luego de los signos de igual:
Query: {
code: 'AR'
}
8. Respaldar una app nodejs en un repositorio Git
- Inicializar un repositorio Git en la carpeta de nuestro proyecto ejecutando:
git init
- Crear un nuevo repo en Github
- Copiarse la url del repo, por ej:
https://github.com/Jonatandb/CursoUdemyIntroNodeJSExpress.git
- Crear en el raíz del proyecto un archivo llamado ".gitignore"
Este archivo va a tener una entrada por cada carpeta y/o archivo que deseamos
que Git no tenga en cuenta, para que no lo versione.
Agregar por ejemplo:
node_modules/
** No agregar nunca el archivo package-lock.json!
- Desde la consola, parados en el raíz del proyecto, ejecutar:
git remote add origin https://github.com/Jonatandb/CursoUdemyIntroNodeJSExpress.git
Con esto vinculamos el repositorio local con el remoto en Github
- Crear rama local:
git checkout -b nombreDeLaRama (por ej: intro o develop, etc)
- Agregar el código y commitearlo:
git add .
git commit -m "Commit inicial"
- Subir el código a Github:
git push origin nombreDeLaRama
9. Instalado express js
Página de Express JS: http://expressjs.com/
Express es un Framework de aplicaciones web Node.js minimalista y flexible que proporciona
un conjunto robusto de características para aplicaciones web y móviles.
Con una gran cantidad de métodos de utilidad HTTP y middlewares a su disposición,
para crear una API robusta, rápida y fácilmente.
Express proporciona una capa delgada de funciones fundamentales de aplicaciones web,
sin ocultar las funciones de Node.js que conoce y ama.
Instalación:
npm install express
Uso básico:
var express = require('express')
const expressWebServer = express()
const port = 3000
expressWebServer.get('/', (req, res) => res.send('Funciona!'))
expressWebServer.listen(port, () => console.log(`Servidor ejecutándose en el puerto ${port}`))
* Con esto, al ejecutar la aplicación, ya queda corriendo un web server capaz de atender
solicitudes, al cual se accede desde el navegor yendo a la url: http://localhost:3000
Al ser una prueba devolverá un error si se intenta acceder a una url diferente de '/'.
Routing (Ruteo): http://expressjs.com/en/guide/routing.html
El enrutamiento se refiere a determinar cómo una aplicación responde a una solicitud del
cliente a un punto final particular (Endpoint), que es una URI (o ruta) y un método de solicitud
HTTP específico (GET, POST, etc.).
Documentación de la API: http://expressjs.com/en/4x/api.html
Instalación de Nodemon:
Paquete NPM que reincia automáticamente la ejecución de nuestro proyecto
cada vez que detecta que el mismo es modificado.
Instalación:
npm install -D nodemon // Con -D le decimos a NPM que instale el paquete solo como una
// dependencia de desarrollo. Una dependencia de desarrollo no
// será incluída cuando se haga un paquete productivo ("build")
// de la aplicación.
Uso:
nodemon index.js
Agregado de un nuevo script al archivo package.json:
"dev" : "nodemon index.js"
* Para el uso del nuevo script ejecutar:
npm run dev
* Npm solo reconoce por defecto los scripts "start" y "test",
por lo que para ejecutar cualquier otro script hace falta
agregar el parámetro run:
npm run dev
npm run miScript
10. Configurando Eslint y Prettier
Al configurar estas herramientas estamos estableciendo que todos los desarroladores involucrados
en el proyecto deben respetar ciertos estandares de escritura y formato de código y a su vez
fuerzan a que el código por todos sea similar.
Instalación como dependencia de desarrollo:
npm i -D eslint
Inicialización de Eslint:
node_modules\.bin\eslint --init
* En linux:
./node_modules/.bin/eslint --init
Va a preguntar que se desea chequear, elegir la opción más completa:
"To check syntax, find problems, and enforce code style "
Va a preguntar que tipo de módulos usa el proyecto, elegir la segunda opción:
"CommonJS (require/exports)"
Va a preguntar si usamos algún framework como React, elegir:
"None of these"
Va a preguntar si usamos TypeScript, elegir:
"N"
Va a preguntar dónde está corriendo nuestro proyecto, elegir:
"Node"
* Si estuvieramos trabajando con React, deberíamos elegir "Browser"
Va a preguntar que estilo de formateo se desea utilizar, elegir:
"Use a popular style guide"
Va a preguntar específicamente que estilo deseamos utilizar, elegir:
"Airbnb: https://github.com/airbnb/javascript"
Va a preguntar como queremos que obtenga el archivo de configuración, elegir:
"JSON"
Va a preguntar si deseamos instalar algunas dependencias necesarias extras, elegir:
"Y"
Con esto se van a instalar las dependencias faltantes y se va a crear un archivo .eslintrc.json
con todo lo configurado.
Instalación de extensiones extra para VSCode:
Buscar e instalar:
Eslint
Prettier
Crear un archivo llamado ".prettierrc" y configurarlo a voluntad:
{
"tabWidth": 4,
"useTabs": true,
"semi": false,
"singleQuote": true
}
Desde VSCode se puede aplicar el formateo presionando:
Alt + Shift + F
o F1 y buscando "Aplicar formato de documento con..." y eligiendo Prettier.
11. Arrow Functions y Firacode
Eslint marca como error cuando se utilizan funciones en lugar de arrow functions, por lo que es recomendable
hacer la migración para también ganar legibilidad en el código.
Para migrar de function a arrow functions:
- Se quita la palabra function
- Se agregar luego de los paréntesis la flecha: =>
- Si la función posee un solo parámetro, el mismo no necesita paréntesis que lo envuelvan:
text => console.log(text)
- La función debe ser asignada a una constante:
const printText = text => console.log(text)
A veces Prettier marca como error cosas como por ejemplo que las arrow functions que reciben un solo parámetro
no tengan el mismo envuelto entre paréntesis. Este aviso se deshabilita agregando al archivo ".eslintrc.json"
en la sección "rules", lo siguiente:
"arrow-parens":"off"
Así como se agrega esta regla según como figura en el popup del alerta de Eslint, se pueden agregar las que se
deseen.
Instalación de fuente Firacode
https://github.com/tonsky/FiraCode
https://github.com/tonsky/FiraCode/wiki/VS-Code-Instructions
Una vez descargada la fuente, darle doble click para que se instale.
Una vez instalada ir a VSCode y agregar lo siguiente en este archivo:
"C:\Users\Jonatandb\AppData\Roaming\Code\User\settings.json"
"editor.fontFamily": "'Fira Code', Consolas, 'Courier New', monospace",
"editor.fontLigatures": true,
// Esto se activa en caso de querer que los simbolos similares se fusionen, por ej ===, =>, etc.
12. Query Params
Con express se pueden obtener fácilmente los parámetros pasados por url consultando
el objeto request.query.
Por ej, al consultar la url "http://localhost:4000/country?code=AR"
el valor del la propiedad query del objeto request será este objeto:
{ code: 'AR' }
Si en lugar de devolver texto (lo que devuelve por default response.send()), queremos
devolver por ejemplo contenido json (Content-Type: application/json), debemos
utilizar el método json() de response:
response.json( ...objeto json a devolver... )
** Al verificarlo detecté que send() parece que ahora detecta cuando recibe un objeto
json y automáticamente devuelve dicho objeto con el Content-Type: "application/json".
13. Alternativa a Query Params
https://www.tutorialspoint.com/expressjs/expressjs_url_building.htm
También se puede utilizar request.params para obtener un objeto que contenga los parámetros
pasados a una url, por ej:
http://localhost:4000/languages/es
Donde, "/languages" es la ruta y "es" es el parámetro.
Esto se obtiene consultando la propiedad de tipo objeto, llamada params, del objeto request:
request.query.params
Para el caso anterior se obtendría:
request.params: { lang: 'es' }
Para que Express intercepte correctamente este tipo de ruta con parámetros, se debe configurar
la misma de la siguiente manera:
app.get('/languages/:lang', (req, res) => {
* Donde ":lang" le dice a Express con que nombre vamos a consultar el valor del parámetro recibido.
Se pueden configurar rutas que reciban múltiples parámetros, de la siguiente manera:
app.get('/languages/:lang/:country', (req, res) => {
* Para consultar ambos valores se debe hacer así:
request.params.lang
request.params.country
14. APIs REST y Postman
API: Application Programming Interface
REST: Representational State Transfer
Una API REST funciona como un navegador:
Un cliente hace una petición mediante el protocolo HTTP a un servidor, el servidor procesa la
solicitud y devuelve una respuesta (ya sea en formato JSON o XML).
* Generalmente las API REST trabajan con el formato JSON como respuesta.
Postman:
Cliente REST recomendado para consumir API's
https://code.tutsplus.com/es/tutorials/a-beginners-guide-to-http-and-rest--net-16340
Verbos HTTP
GET es el tipo más simple de método de solicitud HTTP; La que usan los navegadores cada vez que hace
clic en un enlace o escribe una URL en la barra de direcciones. Indica al servidor que transmita los
datos identificados por la URL al cliente. Los datos nunca deben ser modificados en el lado del
servidor como resultado de una solicitud GET. En este sentido, una petición GET es de sólo lectura,
pero por supuesto, una vez que el cliente recibe los datos, es libre de hacer cualquier operación con
ella por su cuenta, por ejemplo, formatearla para su visualización.
Una petición PUT se utiliza cuando se desea crear o actualizar el recurso identificado por la URL.
Por ejemplo:
PUT /clients/robin
Podría crear un cliente, llamado Robin en el servidor. Usted notará que REST es completamente agnóstico
de servidor; No hay nada en la solicitud que informe al servidor cómo deben crearse los datos, sólo
que debería. Esto le permite intercambiar fácilmente la tecnología del servidor si la necesidad surge.
Las peticiones PUT contienen los datos que se utilizarán para actualizar o crear el recurso en el
cuerpo.
DELETE debe realizar lo contrario de PUT; Debe utilizarse cuando desee eliminar el recurso identificado
por la URL de la solicitud.
POST se utiliza cuando el procesamiento que desea que suceda en el servidor debe repetirse, si la
solicitud POST se repite. Además, las solicitudes POST deben causar el procesamiento del cuerpo de
la solicitud como un subordinado de la URL que está publicando.
Las solicitudes PUT se utilizan fácilmente en lugar de solicitudes POST, y viceversa. Algunos sistemas
utilizan sólo uno, algunos utilizan POST para crear operaciones y PUT para operaciones de actualización
(ya que con una solicitud PUT siempre proporcionan la URL completa), algunos incluso utilizan POST para
actualizaciones y PUT para crear.
15. Definiendo nuestras rutas en un archivo diferente
Se mueve la configuración de las rutas a un archivo independiente dentro de la carpeta routes.
16. Definiendo la estructura de nuestra API
Se crea una carpeta para la versión 1 de las rutas y dentro los archivos "users-routes.js" y "products-routes.js".
Se crea una carpeta para la versión 1 de los controllers y dentro los archivos "users-controller.js" y "products-controller.js"
Middlewares:
https://www.tutorialspoint.com/expressjs/expressjs_middleware.htm
Las funciones de middleware son funciones que tienen acceso al objeto de solicitud (req), el objeto de respuesta (res)
y la siguiente función de middleware en el ciclo de solicitud-respuesta de la aplicación. Estas funciones se utilizan para
modificar los objetos req y res para tareas como analizar cuerpos de solicitud, agregar encabezados de respuesta, etc.
Desde "index.js" se llama a una función exportada dentro del archivo "routes/v1/index.js" que lo que hace es recibir la app de
Express y hace uso del método ".use()" que se utiliza para establecer middlewares a ejecutarse cuando se reciban requests,
entonces se configuran las rutas '/api/v1/users' y '/api/v1/products' y se configura para que la primera utilice
como middleware el archivo "routes/v1/users-routes.js" y a la segunda el archivo "routes/v1/products-routes.js".
Los archivos 'users-routes.js' y 'products-routes.js' a su vez ejecutan la funcionalidad de Express: express.Router() y luego
configuran dicho objeto estableciendo que verbos http se van a atender (post, put, get, etc), por medio de que ruta y que
funcionalidad se debe ejecutar, esta funcionalidad estará en los controllers ('users-controller.js', etc.).
Con esto así configurado y con las funcionalidades establecidas en los controllers, la aplicación ya es capaz de responder
cuando desde "Postman" o "Advanced REST client" se hagan solicitudes a los siguientes endpoints:
GET a http://localhost:4000/api/v1/users/get-all
POST a http://localhost:4000/api/v1/users/create
POST a http://localhost:4000/api/v1/users/update
POST a http://localhost:4000/api/v1/users/delete
GET a http://localhost:4000/api/v1/products/get-all
POST a http://localhost:4000/api/v1/products/create
POST a http://localhost:4000/api/v1/products/delete
17. Obteniendo parámetros desde una petición POST
Body-parser:
https://www.npmjs.com/package/body-parser
Nos permite convertir y obtener los datos entrantes de una solicitud HTTP.
Por ejemplo, cuando se desee crear un usuario, se va a hacer un request a la ruta "/create" y desde ahí necesitamos
acceder al body de la solicitud, que va a tener los datos del usuario que se desea crear.
El request puede ser de varios Content-type, por ejemplo "Form data (www-url-form-encoded)", en este caso los datos enviados
al servidor van a llegar en forma de parámetros (clave, valor) que se pueden leer desde request.body PERO solo si se
instala y configurara body-parser de la siguiente manera:
var bodyParser = require('body-parser')
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))
con esto se puede obtener el contenido de los parámetros mediante request.body, ej:
console.log('req.body', req.body)
lo que devolverá algo como:
req.body [Object: null prototype] { user: 'Jonatandb' }
sino el resultado de request.body será -> undefined.
Este uso es muy poco frecuente, ya que los parámetros y sus valores son tomados todos como del tipo string y a veces eso no
es totalmente conveniente, ya que se necesita en algunos casos enviar datos booleanos o incluso arrays.
Para que la API siga las prácticas recomendadas y sea considerada de calidad debe poder trabajar mediante la recepción de
datos en formato JSON. Para esto habría que configurarla para que sea capaz de recibir y utilizar correctamente la información
contenida en un objeto JSON.
Esto se logra agregando lo siguiente:
// parse application/json
app.use(bodyParser.json())
* Con esto permitimos el consumo de nuestra API utilizando el Content-Type: "application/json"
con esto se puede obtener el contenido del objeto JSON recibido consultado request.body, ej:
console.log('req.body', req.body)
lo que devolverá algo como:
req.body { user: 'Jonatandb' }
Si el request posee un objeto JSON con diferentes tipos de datos en sus parámetros, el resultado obtenido será por ejemplo:
req.body: {
user: 'Jonatandb',
password: '123Abc',
isAdmin: true,
permissions: [ 1, 4, 6 ]
}
Cómo almacenar de forma segura una contraseña:
https://codahale.com/how-to-safely-store-a-password/
bcrypt: Librería que ayuda a hashear passwords.
https://www.npmjs.com/package/bcrypt
Instalación:
npm install bcrypt
Uso asincrónico:
Ya que la funcionalidad que hashea la contraseña (que luego almacenaremos en la base de datos) se demora su tiempo, es
recomendable hacer que toda la funcionalidad de creación de usuario sea asíncrona y aguardar su finalización antes de
devolver una respuesta al usuario (o API) que la está consumiendo.
Para esto, a la función arrow que se ejecuta cuando se accede a la ruta '/api/v1/users/create' le anteponemos la palabra
clave 'async':
const createUser = async (req, res) => {
Luego, en llamada a la función que se demora (la que hashea la contraseña) se antepone la palabra clave 'await':
const hash = await bcrypt.hash(req.body.password, 6)
Con esto, se detendrá la ejecución de la aplicación hasta que el hasheo esté realizado.
Para capturar posibles errores durante el hasheo, se puede envolver el código en un bloque try-catch de la siguiente
manera:
const createUser = async (req, res) => {
try {
const hash = await bcrypt.hash(req.body.password, 6)
res.send('User created!')
} catch (error) {
res.status(500).send({
status: 'ERROR',
message: 'No se pudo hashear la contraseña, error: ' + error.message
// El parámetro error posee siempre una propiedad llamada 'message'
})
}
}
* El uso de la palabra clave await es posible gracias a que la función bcrypt.hash() devuelve una Promise.
* Toda función que hace uso de la palabra clave await debe tener en su definición la palabra clave async.
* Todo error producido en una Promise puede ser capturado mediante el bloque catch de un try-catch.
Con esto garantizamos que nuestra aplicación no se va a "caer" en tiempo de ejecución, esto es, que no va a dejar de responder
por causa de un error.
18. Conectando con Mongodb
Guías oficiales:
Install MongoDB https://docs.mongodb.com/guides/server/install/
Secure your MongoDB Deployment https://docs.mongodb.com/guides/server/auth/
Connect to MongoDB https://docs.mongodb.com/guides/server/drivers/
Read Data from MongoDB https://docs.mongodb.com/guides/server/read/
Connection String URI Format https://docs.mongodb.com/manual/reference/connection-string/#mongodb-uri
MongoDB en 20 minutos - https://www.youtube.com/watch?v=c8n6JsQuX2A
MongoDB Parte 2: Documentos Anidados | Arreglos | Proyecciones | Índices - https://www.youtube.com/watch?v=jK77CnK5DTM&feature=youtu.be
Creación de usuarios:
https://docs.mongodb.com/manual/tutorial/enable-authentication/#create-the-user-administrator
Hacer que luego el servicio se ejecute siempre con "--auth"
https://stackoverflow.com/questions/56110254/how-to-run-mongodb-as-a-service-with-authentication-on-a-windows-machine
BD y otro usuario creado para pruebas:
https://docs.mongodb.com/manual/tutorial/enable-authentication/#create-additional-users-as-needed-for-your-deployment
use cursonodejs
db.createUser({
user: "jonatandb",
pwd: "jonatandb",
roles: [ { role: "readWrite", db: "cursonodejs" } ]
})
* Cadena de conexión:
mongodb://jonatandb:jonatandb@localhost/cursonodejs
Para conectarme, uso el programa Robo 3T 1.3.1 (Ex Robomongo)
Este programa se conecta al servidor local por defecto, apuntando a:
localhost:27017
y permite manipular las bases y los datos.
En la conexión fui a la solapa Authenticación, tildé "Perform authentication" y puse
el nombre de usuario y la clave creados.
Para conectarme por consola, ejecuto:
mongo admin -u superuser -p root
* admin es la base de datos a la que me quiero conectar, si no se especifica se obtiene un error.
** Si inicié el servidor sin la opción "--auth" no es necesario pasar usuario y clave, pero si lo paso funciona igual.
Utilización de variables de entorno
Es importante que nunca se suba al repositorio el archivo con variables de entorno
por la naturaleza de los datos que las mismas contienen, generalmente, cadenas
de conexión, credenciales, y toda información dinámica específica y privada de
nuestra aplicación. Por esta razón es que suele agregar este archivo a la lista
de archivos ingorados por Git dentro del archivo ".gitignore"
Se debe crear un archivo ".env"
Para poder utilizar las variables de entorno, se debe instalar el paquete dotenv:
npm install dotenv
El cual se importa así:
const dotenv = require('dotenv')
Y se debe inicializar así:
dotenv.config();
Esto lee la información dentro del archivo ".env" y la deja a nuestra disponsición para su consulta.
Para consultar el valor de las variables de entorno (junto con las configuradas en el archivo ".env")
se debe consultar la propiedad env del objeto process:
console.log(process.env.nombreDeLaVariable)
* Si la máquina ya tenía una variable de entorno definida con el mismo nombre, se obtiene su valor,
no se va a obtener el que esté en el archivo ·.env".
Para evitar errores cuando por alguna razón no se establezcan correctamente las variables de entorno, se suele
utilizar el simbolo || de la siguiente manera:
const PORT = process.env.PORT || 5555
de esta forma, si no se obtiene el valor de la variable de entorno PORT, se guardará en la constante PORT el valor 5555.
Utilizando Mongoose para conectarse a MongoDB
https://www.npmjs.com/package/mongoose
https://mongoosejs.com/
https://mongoosejs.com/docs/guide.html
Instalación:
npm install mongoose
Uso de Mongoose:
const mongoose = require('mongoose')
mongoose
.connect("mongodb://user:pass@localhost/databasename", {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => {
console.log('Conectado a MongoDB!')
})
.catch(err => {
console.log('Error conectando a MongoDB!', err)
})
19. Definiendo los modelos para nuestra base de datos
En MongoDb se trabaja con colecciones (tablas), y cada colección puede tener documentos (registros).
Guía sobre como definir los esquemas de los documentos:
Defining your schema - https://mongoosejs.com/docs/guide.html#definition
* Cuando se necesita que una colección (tabla) tenga los campos createdAt y updatedAt, se puede pasar en la definición del schema
un segundo objeto con la propiedad "timestamps: true":
const productSchema = new Schema(
{
title: { type: String, required: true },
description: { type: String, required: true },
price: { type: Number, required: true }
},
{
timestamps: true
}
)
y al insertar o modificar datos en esta colección, MongoDb automáticamente va a actualizar tales datos.
20. Guardando usuarios en nuestra base
MongoDB es una base de datos No SQL, por lo tanto puede guardar los datos como si fueran objetos.
Actualicé la versión del servidor de MongoDB
Cree un nuevo usuario superuser, con clave root y un usuario jonatandb con clave jonatandb para hacer pruebas.
https://docs.mongodb.com/manual/tutorial/enable-authentication/#create-the-user-administrator
También modifiqué el archivo de configuración (que es el que usa el servidor ahora que arranca como un servicio) para
que apuente a la carpeta con las bases de datos y se inicie con la opción "--auth", por lo que solo se podrán
conectar al servidor usuarios con permisos.
Actualicé la cadena de conexión en el archivo ".env" para que apuente al nuevo servidor con el nuevo usuario y clave y
a la base de datos creada para este proyecto, llamada cursonodejs:
MONGO=mongodb://jonatandb:jonatandb@localhost/cursonodejs
Se pueden insertar registros de dos maneras:
1) await Users.create({
username, // Es lo mismo que -> username: username
email,
data,
password: hash
})
2) const user = new Users()
user.username = username
user.password = hash
user.email = email
user.data = data
await user.save()
21. Guardando un producto en nuestra base
Cuando MongoDB crea las colecciones en base a los schemas, las crea con los nombres en minúscula y en plural:
Schema: User -> colección en la base de datos: users
Schema: Product -> colección en la base de datos: products
* Esto se puede cambiar, por ejemplo, especificando al momento de la definición del modelo, el nombre de la colección
donde tales documentos serán almacenados:
const model = mongoose.model('User', userSchema, 'Usuarios')
22. Recuperando datos de mongoDB
Para obtener los documentos de una colección se utiliza el método find() del objeto del modelo deseado:
const products = await Product.find()
Esto va a devolver todos los documentos de la colección, con todos sus campos.
Para filtrar los campos que se desean obtener, se utiliza el método .select('lista de campos separados por espacio'):
const products = await Product.find().select('title description price')
En el caso de los campos que sean de tipo "ObjectId", como lo es el campo "user" que almacena un id de usuario,
se puede hacer que MongoDb en lugar de traer ese id traiga el objeto completo al que referencia.
Para esto se utiliza el método .populate('campo que apunta a otro documento'):
const products = await Product.find().populate('user')
** Esto trae todos los campos del documento user vinculado.
Para traer solo los deseados, se pueden especificar los deseados separados por espacio:
const products = await Product.find().populate('user', 'username email data.age role')
** Al haber utilizado .populate(), se agrega a los documentos la propiedad user, por más que no haya sido
especificada en el select.
23. Filtrando datos de mongoDB
Para filtrar los datos obtenidos, se le pasa al método find() un objeto con las condiciones deseadas:
getProductsByUserId():
const products = await Product.find({
user: req.params.userId
})
getProductsCheaperThan100():
const products = await Product.find({
price: { $lt: 100 }
})
getProductsGreaterThan100():
const products = await Product.find({
price: { $gt: 100 }
})
Para una lista de filtros aplicables consultar la documentación de MongoDB:
https://docs.mongodb.com/manual/reference/operator/query/
Comparison
Name Description
$eq Matches values that are equal to a specified value.
$gt Matches values that are greater than a specified value.
$gte Matches values that are greater than or equal to a specified value.
$in Matches any of the values specified in an array.
$lt Matches values that are less than a specified value.
$lte Matches values that are less than or equal to a specified value.
$ne Matches all values that are not equal to a specified value.
$nin Matches none of the values specified in an array.
Logical
Name Description
$and Joins query clauses with a logical AND returns all documents that match the conditions of both clauses.
$not Inverts the effect of a query expression and returns documents that do not match the query expression.
$nor Joins query clauses with a logical NOR returns all documents that fail to match both clauses.
$or Joins query clauses with a logical OR returns all documents that match the conditions of either clause.
Y la lista continúa...
24. Actualizando información en la base
Utilización del método findByIdAndUpdate( "id del documento a actualizar", { "objeto con propiedades a actualizar "}):
const { userId, email, data } = req.body
await User.findByIdAndUpdate(userId, {
email,
data
})
25. Autenticación de un usuario
Hasta el momento en el archivo user-routes.js están las rutas para actualizar (/update) y eliminar un usuario (/delete), pero
éstas rutas no tienen ningún tipo de seguridad por lo que cualquiera podría consumir estas rutas y modificar la información
de un usuario y también eliminarlo.
Para darle seguridad a la API se debe crear una ruta de autenticación (/login).
La nueva ruta "/login" se define como POST, porque si se definiera como GET entonces la información de email y password
viajaría como texto plano y podría ser interceptada y podría ser hackeada sin importar si nosotros tenemos habilitado un
certificado SSL y estamos consumiendo nuestra API REST utilizando HTTPS. Por lo tanto si tenemos habilitado un certificado
SSL y estamos consumiendo nuestra API REST utilizando HTTPS es recomendable enviar la información sensible por POST.
Para buscar un usuario utilizamos el método findOne() del modelo User.
findOne() devuelve el primer usuario que coincida con la búsqueda, por ej:
const { email, password } = req.body
const user = await User.findOne({ email })
Luego verificamos que el password ingresado es válido, utilizando le método compare() de bcrypt:
const isOk = await bcrypt.compare(password, user.password)
26. Json Web Tokens
Las rutas /delete y /update deben tener una especie de validación de autenticación para garantizar que no cualquiera consuma
éstas rutas.
Por ejemplo la ruta /delete debería ser consumida solo por un usuario con permisos de administrador
La ruta /update solo debería ser consumida por el usuario dueño de la cuenta
Para establecer esta seguridad se utiliza por ejemplo Json Web Token:
https://jwt.io/
Json Web Token (o JWT) básicamente es un string codificado en el cual podemos almacenar cierta información.
Para generar este token se debe utilizar una firma que solamente debe ser guardada en el servidor (jamas en el lado del
cliente, jamas en la aplicación movil o en el browser, SIEMPRE del lado del servidor)
Entonces para que alguien pueda consumir alguna de las rutas mencionadas, debe enviar el token, el cual verificamos si fue
firmado por nuestro "secret" y luego verificamos si todavía es un token vigente o si está expirado.
Cuando generamos un JWT podemos darle un tiempo de expiración.
* Es recomendable siempre ponerle un tiempo de expiración corta al token generado, porque si retornamos un token que
nunca expire no podemos saber si la persona que usó la ruta utilizó buenas prácticas de programación para almacenar
de manera segura este token en su equipo. Si alguien le roba el token, y este token no tiene un tiempo de vida corto,
automáticamente él ladrón podrá seguir consumiendo las rutas de delete o update durante todo ese tiempo restante.
Instalación:
npm install jsonwebtoken
Generación de un jwt:
Desde por ejemplo el método login, una vez que verificamos que el usuario se logueó correctamente, ejecutamos:
const jwt = require('jsonwebtoken') // Esto va fuera del método login()
const token = jwt.sign(
{
userId: user._id, role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: 60 * 10 }
)
res.send({ status: 'OK', data: { token, expiresIn } })
* El método sign de jwt espera tres parámetros para generar el token:
Primero, la información que contendrá el token generado (Jamás debe incluir información sensible, solo lo necesario)
Segundo, la firma (el secret) con el cual firmar el token
Tercero, el tiempo en cuanto el token expirará
El token generado, que en este ejemplo se está devolviendo al navegador como valor de la propiedad data, será el que se
utilizará para consumir las rutas de /delete o /update.
27. Middlewares
Un middleware es una función (que recibe tres parámetros: request, response y nextFunction) y luego de hacer su trabajo (ya
sea verificar algo, loguear, etc) ejecuta (si corresponde hacerlo) la función referenciada por el parámentro nextFunction.
Generalmente se crea una carpeta "middlewares" y dentro un archivo con el nombre del mismo, ej:
isAuth.js
Este archivo exporta el middleware (la función) para poder consumirla desde cualquier parte de nuestro proyecto.
Para aplicar este middleware por ejemplo en la ruta '/update', se debe hacer una importanción parcial del mismo:
const { isAuth } = require('../../middlewares/isAuth')
Y luego se debe pasar como segundo parámetro de la función post():
router.post('/update', isAuth, usersController.updateUser)
Configurado así, cada vez que se acceda a la ruta '/update', primero se va a ejecutar el middleware isAuth y si éste considera
que corresponde continuar con la ejecución entonces (por ejemplo verificando si el usuario tiene los permisos necesarios
para actualizar los datos) ejecutará: nextFunction(), lo que hará que efectivamente se ejecute así la funcionalidad
que está dentro del controlador usersController.updateUser().
Así como al método post() se le pasa luego del path el middleware isAuth, se pueden pasar n cantidad de middlewares:
router.post('/update', isAuth, isValidHostname, logger, usersController.updateUser())
Es importante a la hora de crear nuestros middlewares que éstos devuelvan algo, esto es o (1) llamar a la función nextFunction
o (2) devolver una respuesta que indique por qué se detuvo la ejecución del request, ej:
response.status(403).send({status:'ACCESS_DENIED', message: ''})
Cuando un middleware ejecuta nextFunction() lo que se hace es que se pasa a la ejecución del siguiente middleware en la cadena
de middlewares. En este ejemplo, luego de ejecutarse isAuth se ejecutaría isValidHostname, luego logger y finalmente el
método updateUser() del controlador usersController.
Siempre un middleware tiene en el parámetro nextFunction una referencia a lo siguiente que se debería ejecutar si la ejecución
debe continuar.
Creación del middleware isAuth:
El mismo verifica que en el request venga un header llamado token
También verifica que el token sea válido y no haya expirado, haciendo uso del método jwt.verify(token, secret)
Éste método arroja una excepción ante cualquier problema con el token (vencido, inválido, etc)
Ahora que se creó el middleware isAuth, que verifica que en cada request realizado a la ruta '/update' venga en los headers
uno con nombre 'token', con un token jwt válido, se debe hacer cada request a esta ruta con un token obtenido al momento
de hacer login, sino no se puede ejecutar el update de los datos del usuario.
* Siempre que a un request se agrega un header, el mismo debe estar todo en minúscula y no puede tener espacios (pero si
puede tener guiones, ej: token-de-acceso)
Creación del middleware isValidHostname:
Valida que se esté consumiendo la API desde ciertos hosts válidos.
Incialmente solo verifica que los requests vengan desde localhost, pero luego se pueden agregar IP's deseadas específicas
a la lista de hostnames considerados válidos de los que se desea que se pueda consultar a la API:
const isValidHostname = (req, res, next) => {
//console.log(req.hostname) // Acá viene por ejemplo 'localhost'.
const validHosts = ['localhost'] // Acá agregaría todas las IP's de los consumidores permitidos.
if (validHosts.includes(req.hostname)){
next()
} else {
res.status(403).send({ status: 'ACCESS_DENIED' })
}
}
28. Middlewares 2
Cuando se ejecuta en el middleware isAuth el método jwt.verify(token, secret), si el token es válido se obtiene como
resultado un objeto que contiene los datos que se agregaron al token cuando el mismo fue firmado en login().
Utilizando ese objeto obtengo el userId y el rol del usuario que se logueó y está utilizando el sistema.
29. Middlewares 3
Debido a que el middleware isAuth se ejecuta antes que el controller de updateUser, es posible guardar información
en el objeto request que luego va a poder ser accedida por el controller.
Haciendo esto, se puede guardar por ejemplo la información que fue incluída en el token y como el mismo posee
el userId del usuario que se desea actualizar, se puede omitir este dato en el body del request.
** Cuando se agregan datos al request, es muy importante recordar que no se puede hacer en propiedades existentes,
por lo tanto no se puede guardar información en:
request.body
request.headers
request.location
ni en ninguna otra propiedad de las demás reservadas.
Se debe usar siempre alguna nueva, por ej:
request.sessionData = { "clave": "valor"}
30. Eliminando usuarios
Actualización del middleware isAuth, para que en caso de que el login del usuario sea correcto, se almacene
en la nueva propiedad sessionData del objeto request, además del userid, el rol (role):
req.sessionData = { userId: data.userId, role: data.role }
Creación de un nuevo middleware llamado isAdmin, el cual solo continúa con la ejecución cuando el rol del
usuario logueado es administrador.
Verifica la nueva propiedad role del objeto sessionData.
Desarrollo del cuerpo del controller deleteUser, el cual obtiene del body del request el valor de una nueva
propiedad llamada userId que debe tener el id del usuario a eliminar.
Luego, utilizando dicho id de usuario, se elimina el mismo y también los productos que tuviera asociados:
await User.findByIdAndRemove(userId)
await Product.deleteMany({ user: userId })
Para permitir que solo pueda eliminar un usuario un administrador, se configuró la ruta '/delete' como sigue:
router.post('/delete', isValidHostname, isAuth, isAdmin, usersController.deleteUser)
donde solo se va a ejecutar la eliminación si:
- Se hace el post desde un hostname válido
- El usuario que hace el post especificó en los headers un token válido
- El usuario tiene el rol admin
- En el body del request se especificó el id del usuario mediante la propiedad userId:
{
"userId": "5e5acdc36a84c725dc3ef99e"
}
31. Recuperando los usuarios registrados
Cuando se obtienen los documentos de una colección de MongoDB, se puede utilizar el método find() que lo que
hará será devolver todos los documentos, pero de tales documentos se obtendrán todos sus campos y esto
es implica un error de seguridad por mostrarse el valor de campos con datos sensibles como password.
Esto puede evitarse especificando que campos se desean obtener mediante el agregado de la llamada al
método .select() luego de la llamada a find().
.select() permite que se le pase una cadena de texto con los nombres de los campos a obtener, separados
por espacio, ej:
Users.find().select("email data") // Todos los documentos devueltos solo tendrán estos campos
Pero si nuestro documento tiene muchos campos, es tedioso especificar todos y cada uno, por lo que se
puede utilizar una característica facilitada por el método select que es especificar mediante un
objeto JSON que campos serán traídos o cuales no, especificando un valor numérico para cada uno,
si tal campo debe ser traído se indicará con un uno, sino con un cero:
Users.find().select({ password: 0, __v: 0, role: 0}) // Ningún documento tendrá estos campos
En este caso esos tres campos no se devolverán cuando se obtengan los usuarios.
* Hay que tener en cuenta que no se puede mezclar la especificidad de campos a traer y campos que
no se desean traer, por lo tanto o siempre se indican con un valor de uno, para que se traigan solo
esos campos, o siempre se especifica un cero para que esos campos específicos no se traigan.
Implementé un logger que muestra resultados en la consola solo si no se está en modo productivo.
32. Configurando TypeScript en nuestro proyecto
TypeScript es un lenguaje tipado, que permite detectar errores antes de compilar el código.
Establece un standard de codificación ya que todos los métodos que reciban parámetros tendrán que tener
definido el tipo de dato de cada parámetro y si retornan un dato deberán especificar de que tipo es el retorno.
Instalando TypeScript globalmente en la pc de desarrollo:
- npm install typescript -g
Instalando TypeScript como dependencia de desarrollo ya que no se utiliza en producción:
- npm install -D ts-node typescript
ts-node https://www.npmjs.com/package/ts-node
ts-node es un paquete npm que permite al usuario ejecutar archivos con código typescript directamente,
sin la necesidad de precompilación usando tsc. También proporciona REPL.
Instalación global:
npm install -g ts-node
* ts-node no incluye el compilador de escritura de tipos, por lo que es posible que haya que instalarlo:
npm install -g typescript
Ejecutando script:
Para ejecutar un script llamado main.ts:
// main.ts
console.log("Hello world");
Ejemplo de uso:
ts-node main.ts Salida -> Hello world
Creando archivo de configuración de TypeScript para el proyecto:
- tsc --init
Esto crea un archivo "tsconfig.json" el cual debe ser modificado, dejándolo así:
"target": "es6", Especifico que utilizo ECMAScript 6.
"outDir": "./dist", Especifico que convierta los archivos typescript a js en la carpeta dist.
"rootDir": "./src", Especifico carpeta donde están los archivos typescript.
"moduleResolution": "node", Especifico que estoy utilizando Node.
Actualización de scripts en package.json:
"start": "node dist/index.js", Esto se creará cuando se ejecute npm run build en base al código TypeScript.
"dev": "nodemon src/index.ts",
"build": "tsc -p .", Esto transpila (convierte) todo el contenido typescript a js en la carpeta dist.
Reestructuré las carpetas para que todo el código quede dentro de la carpeta src.
Cree el archivo src/index.ts
Ejecuté npm run build y se generó la carpeta dist con el archivo index.js vacío, ya que el se generó en base al
archivo index.ts el cual aún no tiene contenido alguno.
Actualización de configuración de Eslint para que trabaje bien con TypeScript: