visual usando studio run proyecto node cómo crear app node.js express architecture

node.js - usando - Creación de una aplicación empresarial con Node/Express



node js visual studio 2017 (5)

Estoy tratando de entender cómo estructurar la aplicación empresarial con Node / Express / Mongo (en realidad usando MEAN stack).

Después de leer 2 libros y buscar en Google (incluidas preguntas similares de StackOverflow), no pude encontrar ningún buen ejemplo de estructuración de aplicaciones grandes con Express. Todas las fuentes que he leído sugieren dividir la aplicación por las siguientes entidades:

  • rutas
  • controladores
  • modelos

Pero el problema principal que veo con esta estructura es que los controladores son como objetos de dios, saben sobre los objetos req , res , responsables de la validación y tienen lógica comercial incluida.

Por otro lado, las rutas me parecen una ingeniería excesiva porque lo único que hacen es asignar puntos finales (caminos) a los métodos del controlador.

Tengo antecedentes de Scala / Java, por lo que tengo la costumbre de separar toda la lógica en 3 niveles: controlador / servicio / dao.

Para mí las siguientes declaraciones son ideales:

  • Los controladores son responsables solo de interactuar con la parte WEB, es decir, ordenar / desarmar, alguna validación simple (requerida, min, max, regex de correo electrónico, etc.);

  • La capa de servicio (que en realidad me perdí en las aplicaciones NodeJS / Express) es responsable solo de la lógica comercial, algunas validaciones comerciales. La capa de servicio no sabe nada sobre la parte WEB (es decir, se puede llamar desde otro lugar de aplicación, no solo desde el contexto web);

  • En cuanto a la capa DAO, todo está claro para mí. Los modelos de mangosta son en realidad DAO, así que lo más claro para mí aquí.

Creo que los ejemplos que he visto son muy simples, y muestran solo conceptos de Nodo / Expreso, pero quiero ver algún ejemplo del mundo real, con mucha de la lógica / validación empresarial involucrada.

EDITAR:

Otra cosa que no me queda clara es la ausencia de objetos DTO. Considere este ejemplo:

const mongoose = require(''mongoose''); const Article = mongoose.model(''Article''); exports.create = function(req, res) { // Create a new article object const article = new Article(req.body); // saving article and other code }

El objeto JSON de req.body se pasa como parámetro para crear el documento Mongo. Me huele mal. Me gustaría trabajar con clases concretas, no con JSON sin procesar

Gracias.


Cada uno tiene su propia forma de dividir el proyecto en ciertas carpetas. la estructura que uso es

  • config
  • registros
  • rutas
  • controladores
  • modelos
  • servicios
  • utils
  • app.js / server.js / index.js (cualquier nombre que prefiera)

La carpeta de configuración contiene archivos de configuración como la configuración de conexión de la base de datos para todas las fases de desarrollo como "producción", "desarrollo", "prueba"

ejemplo

''use strict'' var dbsettings = { "production": { //your test settings }, "test": { }, "development": { "database": "be", "username": "yourname", "password": "yourpassword", "host": "localhost", "connectionLimit": 100 } } module.exports = dbsettings

la carpeta de registro contiene sus registros de error de registros de conexión para la depuración

el controlador es para validar sus datos de solicitud y lógica de negocios

ejemplo

const service = require("../../service") const async = require("async") exports.techverify = (data, callback) => { async.series([ (cb) => { let searchObject = { accessToken: data.accessToken } service.admin.get(searchObject, (err, result) => { if (err || result.length == 0) { callback(err, { message: "accessToken is invalid" }) } else { delete data.accessToken service.tech.update(data, { verified: true }, (err, affe, res) => { if (!err) callback(err, { message: "verification done" }) else callback(err, { message: "error occured" }) }) } }) } ]) }

modelos es para definir su esquema de base de datos

ejemplo de esquema mongoDb

''use strict'' let mongoose = require(''mongoose''); let schema = mongoose.Schema; let user = new schema({ accesstoken: { type: String }, firstname: { type: String }, lastname: { type: String }, email: { type: String, unique: true }, image: { type: String }, phoneNo: { type: String }, gender: { type: String }, deviceType: { type: String }, password: { type: String }, regAddress: { type: String }, pincode: { type: String }, fbId: { type: String, default: 0 }, created_at: { type: Date, default: Date.now }, updated_at: { type: Date, default: Date.now }, one_time_password: { type: String }, forgot_password_token: { type: String }, is_block: { type: Boolean, default: 0 }, skin_type: { type: String }, hair_length: { type: String }, hair_type: { type: String }, credits: { type: Number, default: 0 }, invite_code: { type: String }, refered_by: { type: String }, card_details: [{ card_type: { type: String }, card_no: { type: String }, card_cv_no: { type: String }, created_at: { type: Date } }] }); module.exports = mongoose.model(''user'', user);

los servicios son para escribir su consulta de base de datos, evite escribir consultas en el controlador, intente escribir una consulta en esta carpeta y llámela al controlador

consultas con mangosta

''use strict'' const modelUser = require(''../../models/user''); exports.insert = (data, callback) => { console.log(''mongo log for insert function'', data) new modelUser(data).save(callback) } exports.get = (data, callback) => { console.log(''mongo log for get function'', data) modelUser.find(data, callback) } exports.update = (data, updateData, callback) => { console.log(''mongo log for update function'', data) modelUser.update(data, updateData, callback); } exports.getWithProjection = (data, projection, callback) => { console.log(''mongo log for get function'', data) modelUser.find(data, projection, callback) }

utils es para la función de utilidad común que se usa comúnmente en su proyecto, tal como cifrar, descifrar contraseña, etc.

ejemplo

exports.checkPassword = (text, psypherText) => { console.log("checkPassword executed") console.log(text, psypherText) return bcrypt.compareSync(text, psypherText) } exports.generateToken = (userEmail) => { return jwt.sign({ unique: userEmail, timeStamp: Date.now }, config.keys.jsonwebtoken) }


He logrado lo que intentas hacer. Esencialmente falta una capa en la estructura tradicional de Ruta / Controlador / Modelo. La respuesta breve es que esto aún no ha evolucionado en el reino de los nodos como lo desea, por lo que hay cosas personalizadas que hacer si desea manipular objetos.

Un par de sugerencias para comenzar:

  • Use TypeScript en lugar de stock JavaScript
  • Reemplazar Express con HapiJS

La forma más eficiente que encontré para lograr este objetivo es tener un objeto que tenga métodos estáticos que accedan al modelo y luego los importen a sus controladores. Ahora, esto toma más tiempo para la configuración que simplemente seguir los documentos en los servidores de nodos, pero una vez que haya terminado, es muuuuuuuuy fácil de mantener y la división del trabajo para equipos más grandes es fantástica (una vez que el equipo puede dedicarse literalmente a rutas / controladores mientras otro maneja los DAO / modelos).

// controller import Article from ''models/Article''; export ArticleController { class GET { handler( req, res ){ return Article.find(req.params.id); } } class POST { validator: { // this is where you ensure req.payload is going to be sufficient for the article constructor payload: { name: joi.string().required() } } handler( req, res ){ const oArticle = new Article(req.payload); oArticle.save(); } } } //Article export class Article { public id: string; public name: string; constructor(data){ // over-simplified logic to load data into object for example // there are some edge cases you need to figure out Object.assign(this, data); } public static find( id ){ // get the article from your DAO - pseudo code const data = DAO.getArticleDataById(id); return new Article(data); } public save(){ // save this object using DAO } }


La respuesta de Rohit Salaria básicamente explica la misma estructura de aplicación a la que estás acostumbrado en Java.

  • Los controladores son los controladores en Java
  • Los modelos son la capa de acceso a datos
  • Los servicios son la capa de servicio

Sin embargo, tengo algunas observaciones. La primera y más importante es que esto no es Java. Puede sonar obvio, pero solo mire su pregunta y vea que está buscando la misma experiencia de desarrollo con los mismos conceptos que utilizó en el mundo de Java. Mis siguientes comentarios son solo la explicación de esto.

DTO que faltan. En Java solo son necesarios, punto. En una aplicación web Java, donde almacena sus datos en una base de datos relacional y envía y recibe datos al front-end en JSON, es natural que convierta los datos en un objeto Java. Sin embargo, en una aplicación Node todo es javascript y JSON. Ese es uno de los puntos fuertes de la plataforma. Dado que JSON es el formato de datos común, no es necesario escribir código ni depender de bibliotecas para traducir entre el formato de datos de sus capas.

Pasar el objeto de datos directamente desde la solicitud al modelo. Por qué no? Tener JSON como el formato de datos común desde el front-end hasta la base de datos le permite sincronizar fácilmente el modelo de datos de su aplicación entre todas sus capas. Por supuesto, no tiene que ir por este camino, pero es suficiente la mayor parte del tiempo, entonces, ¿por qué no usarlo? En cuanto a la validación, se realiza en el modelo, donde pertenece de acuerdo con la teoría MVC (y no en el controlador donde la pereza y el pragmatismo a menudo lo ponen :)).

Para el pensamiento final que quiero agregar, que esta no es la mejor plataforma cuando se trata de escalar el tamaño del proyecto. Es un gesto negativo, pero Java es mejor en ese aspecto.


Los controladores son objetos de Dios hasta que no quieras que lo sean ...
- no dices zurfyx (╯ ° □ °) ╯︵ ┻━┻

¿Solo te interesa la solución? Salta a la última sección "Resultado" .

┬──┬◡ ノ (° - ° ノ)

Antes de comenzar con la respuesta, permítame disculparme por hacer que esta respuesta sea mucho más larga que la longitud SO habitual. Los controladores solos no hacen nada, se trata de todo el patrón MVC. Por lo tanto, sentí que era relevante revisar todos los detalles importantes sobre el modelo de enrutador <-> Controlador <-> Servicio <->, para mostrarle cómo lograr controladores aislados adecuados con responsabilidades mínimas.

Caso hipotético

Comencemos con un pequeño caso hipotético:

  • Quiero tener una API que sirva una búsqueda de usuarios a través de AJAX.
  • Quiero tener una API que también sirva a la misma búsqueda de usuarios a través de Socket.io.

Comencemos con Express. Eso es fácil, ¿verdad?

routes.js

import * as userControllers from ''controllers/users''; router.get(''/users/:username'', userControllers.getUser);

controllers / user.js

import User from ''../models/User''; function getUser(req, res, next) { const username = req.params.username; if (username === '''') { return res.status(500).json({ error: ''Username can/'t be blank'' }); } try { const user = await User.find({ username }).exec(); return res.status(200).json(user); } catch (error) { return res.status(500).json(error); } }

Ahora hagamos la parte Socket.io:

Como esa no es una pregunta de socket.io , omitiré la repetitiva.

import User from ''../models/User''; socket.on(''RequestUser'', (data, ack) => { const username = data.username; if (username === '''') { ack ({ error: ''Username can/'t be blank'' }); } try { const user = User.find({ username }).exec(); return ack(user); } catch (error) { return ack(error); } });

Uhm, algo huele aquí ...

  • if (username === '''') . Tuvimos que escribir el validador del controlador dos veces. ¿Qué pasaría si hubiera n validadores de controlador? ¿Tendríamos que mantener dos (o más) copias de cada una actualizada?
  • User.find({ username }) se repite dos veces. Eso posiblemente podría ser un servicio.

Acabamos de escribir dos controladores que se adjuntan a las definiciones exactas de Express y Socket.io respectivamente. Lo más probable es que nunca se rompan durante su vida útil, ya que tanto Express como Socket.io tienden a tener compatibilidad con versiones anteriores. PERO , no son reutilizables. Cambio Express para Hapi ? Tendrá que rehacer todos sus controladores.

Otro mal olor que podría no ser tan obvio ...

La respuesta del controlador es artesanal. .json({ error: whatever })

Las API en RL cambian constantemente. En el futuro, es posible que desee que su respuesta sea { err: whatever } o tal vez algo más complejo (y útil) como: { error: whatever, status: 500 }

Comencemos (una posible solución)

No puedo llamarlo la solución porque hay una cantidad infinita de soluciones por ahí. Depende de su creatividad y sus necesidades. La siguiente es una solución decente; Lo estoy usando en un proyecto relativamente grande y parece estar funcionando bien, y soluciona todo lo que señalé antes.

Iré Modelo -> Servicio -> Controlador -> Enrutador, para mantenerlo interesante hasta el final.

Modelo

No entraré en detalles sobre el Modelo, porque ese no es el tema de la pregunta.

Debería tener una estructura Mongoose Model similar a la siguiente:

modelos / Usuario / validate.js

export function validateUsername(username) { return true; }

Puede leer más sobre la estructura apropiada para los validadores de mangosta 4.x here .

modelos / Usuario / index.js

import { validateUsername } from ''./validate''; const userSchema = new Schema({ username: { type: String, unique: true, validate: [{ validator: validateUsername, msg: ''Invalid username'' }], }, }, { timestamps: true }); const User = mongoose.model(''User'', userSchema); export default User;

Solo un esquema de usuario básico con un campo de nombre de usuario y campos updated controlados por mangostas.

La razón por la que incluí el campo de validate aquí es para que te des cuenta de que deberías estar haciendo la mayor parte de la validación del modelo aquí, no en el controlador.

Mongoose Schema es el último paso antes de llegar a la base de datos, a menos que alguien consulte a MongoDB directamente, siempre tendrá la seguridad de que todos pasan por las validaciones de su modelo, lo que le brinda más seguridad que colocarlos en su controlador. No quiere decir que los validadores de pruebas unitarias como están en el ejemplo anterior son triviales.

Lea más sobre esto here y here .

Servicio

El servicio actuará como el procesador. Dados los parámetros aceptables, los procesará y devolverá un valor.

La mayoría de las veces (incluido este), utilizará Mongoose Models y devolverá una Promise (o una devolución de llamada; pero definitivamente usaría ES6 con Promesas si aún no lo está haciendo).

services / user.js

function getUser(username) { return User.find({ username}).exec(); // Just as a mongoose reminder, .exec() on find // returns a Promise instead of the standard callback. }

En este punto te estarás preguntando, ¿no hay bloqueo? No, porque haremos un truco genial más tarde y no necesitamos uno personalizado para este caso.

Otras veces, un servicio de sincronización trivial será suficiente. Asegúrese de que su servicio de sincronización nunca incluya E / S, de lo contrario, bloqueará todo el hilo Node.js.

services / user.js

function isChucknorris(username) { return [''Chuck Norris'', ''Jon Skeet''].indexOf(username) !== -1; }

Controlador

Queremos evitar controladores duplicados, por lo que solo tendremos un controlador para cada acción.

controllers / user.js

export function getUser(username) { }

¿Cómo se ve esta firma ahora? Bonita, verdad? Debido a que solo estamos interesados ​​en el parámetro de nombre de usuario, no necesitamos tomar cosas inútiles como req, res, next .

Agreguemos los validadores y el servicio que faltan:

controllers / user.js

import { getUser as getUserService } from ''../services/user.js'' function getUser(username) { if (username === '''') { throw new Error(''Username can/'t be blank''); } return getUserService(username); }

Todavía se ve bien, pero ... ¿qué pasa con el throw new Error , no hará que mi aplicación se bloquee? - Shh, espera. Aún no hemos terminado.

Entonces, en este punto, la documentación de nuestro controlador se vería algo así:

/** * Get a user by username. * @param username a string value that represents user''s username. * @returns A Promise, an exception or a value. */

¿Cuál es el "valor" indicado en los @returns ? ¿Recuerdas que anteriormente dijimos que nuestros servicios pueden ser tanto sincronizados como asíncronos (usando Promise )? getUserService es asíncrono en este caso, pero el servicio de isChucknorris no lo haría, por lo que simplemente devolvería un valor en lugar de una Promesa.

Esperemos que todos lean los documentos. Porque necesitarán tratar a algunos controladores diferentes a otros, y algunos de ellos requerirán un bloqueo try-catch .

Como no podemos confiar en los desarrolladores (esto me incluye a mí) que lee los documentos antes de intentarlo primero, en este punto tenemos que tomar una decisión:

  • Controladores para forzar un retorno Promise
  • Servicio para devolver siempre una promesa

⬑ Esto resolverá el retorno inconsistente del controlador (no el hecho de que podamos omitir nuestro bloque try-catch).

OMI, prefiero la primera opción. Porque los controladores son los que encadenarán más Promesas la mayoría de las veces.

return findUserByUsername .then((user) => getChat(user)) .then((chat) => doSomethingElse(chat))

Si usamos ES6 Promise, también podemos utilizar una buena propiedad de Promise para hacerlo: Promise puede manejar las no promesas durante su vida útil y seguir devolviendo una Promise :

return promise .then(() => nonPromise) .then(() => // I can keep on with a Promise.

Si el único servicio que llamamos no usa Promise , podemos hacer uno nosotros mismos.

return Promise.resolve() // Initialize Promise for the first time. .then(() => isChucknorris(''someone''));

Volviendo a nuestro ejemplo resultaría en:

... return Promise.resolve() .then(() => getUserService(username));

En realidad, no necesitamos Promise.resolve() en este caso, ya que getUserService ya devuelve una Promesa, pero queremos ser coherentes.

Si se está preguntando sobre el bloque catch : no queremos usarlo en nuestro controlador a menos que queramos un tratamiento personalizado. De esta forma, podemos hacer uso de los dos canales de comunicación ya integrados (la excepción de los errores y el retorno de los mensajes de éxito) para entregar nuestros mensajes a través de canales individuales.

En lugar de ES6 Promise .then , podemos hacer uso del nuevo ES2017 async / await wait ( ahora oficial ) en nuestros controladores:

async function myController() { const user = await findUserByUsername(); const chat = await getChat(user); const somethingElse = doSomethingElse(chat); return somethingElse; }

Observe async delante de la function .

Enrutador

Finalmente el enrutador, ¡yay!

Entonces, aún no hemos respondido nada al usuario, todo lo que tenemos es un controlador que sabemos que SIEMPRE devuelve una Promise (con suerte con datos). ¡Oh !, y eso posiblemente puede arrojar una excepción si throw new Error is called o se rompe algún servicio Promise .

El enrutador será el que, de manera uniforme, controlará las peticiones y devolverá datos a los clientes, ya sean datos existentes, datos null o undefined o un error.

El enrutador será el ÚNICO que tendrá múltiples definiciones. El número de los cuales dependerá de nuestros interceptores. En el caso hipotético, estos fueron API (con Express) y Socket (con Socket.io).

Repasemos lo que tenemos que hacer:

Queremos que nuestro enrutador se convierta (req, res, next) en (username) . Una versión ingenua sería algo como esto:

router.get(''users/:username'', (req, res, next) => { try { const result = await getUser(req.params.username); // Remember: getUser is the controller. return res.status(200).json(result); } catch (error) { return res.status(500).json(error); } });

Aunque funcionaría bien, eso daría lugar a una gran cantidad de duplicación de código si copiamos y pegamos este fragmento en todas nuestras rutas. Entonces tenemos que hacer una mejor abstracción.

En este caso, podemos crear una especie de cliente de enrutador falso que toma una promesa n parámetros y realiza sus tareas de enrutamiento y return , tal como lo haría en cada una de las rutas.

/** * Handles controller execution and responds to user (API Express version). * Web socket has a similar handler implementation. * @param promise Controller Promise. I.e. getUser. * @param params A function (req, res, next), all of which are optional * that maps our desired controller parameters. I.e. (req) => [req.params.username, ...]. */ const controllerHandler = (promise, params) => async (req, res, next) => { const boundParams = params ? params(req, res, next) : []; try { const result = await promise(...boundParams); return res.json(result || { message: ''OK'' }); } catch (error) { return res.status(500).json(error); } }; const c = controllerHandler; // Just a name shortener.

Si está interesado en saber más sobre este truco , puede leer sobre la versión completa de este en mi otra respuesta en React-Redux y Websockets con socket.io (sección "SocketClient.js").

¿Cómo se vería tu ruta con el controllerHandler ?

router.get(''users/:username'', c(getUser, (req, res, next) => [req.params.username]));

Una línea limpia, como en el principio.

Otros pasos opcionales

Controlador de promesas

Solo se aplica a aquellos que usan ES6 Promises. La versión async / await ES2017 ya me parece buena.

Por alguna razón, no me gusta tener que usar el nombre Promise.resolve() para construir la Promesa de inicialización. Simplemente no está claro qué está pasando allí.

Prefiero reemplazarlos por algo más comprensible:

const chain = Promise.resolve(); // Write this as an external imported variable or a global. chain .then(() => ...) .then(() => ...)

Ahora sabes que la chain marca el comienzo de una cadena de Promesas. Al igual que todos los que leen su código, o si no, al menos asumen que es una cadena de funciones de servicio.

Manejador de error expreso

Express tiene un controlador de errores predeterminado que debe utilizar para capturar al menos los errores más inesperados.

router.use((err, req, res, next) => { // Expected errors always throw Error. // Unexpected errors will either throw unexpected stuff or crash the application. if (Object.prototype.isPrototypeOf.call(Error.prototype, err)) { return res.status(err.status || 500).json({ error: err.message }); } console.error(''~~~ Unexpected error exception start ~~~''); console.error(req); console.error(err); console.error(''~~~ Unexpected error exception end ~~~''); return res.status(500).json({ error: ''⁽ƈ ͡ (ुŏ̥̥̥̥םŏ̥̥̥̥) ु'' }); });

Además, probablemente debería usar algo como debug o winston lugar de console.error , que son formas más profesionales de manejar registros.

Y así es como conectamos esto al controllerHandler :

... } catch (error) { return res.status(500) && next(error); }

Simplemente estamos redirigiendo cualquier error capturado al controlador de errores de Express.

Error como ApiError

Error se considera la clase predeterminada para encapsular los errores al lanzar una excepción en Javascript. Si realmente solo desea realizar un seguimiento de sus propios errores controlados, probablemente cambiaría el throw Error y el controlador de Error Express de Error a ApiError , e incluso puede hacer que se adapte mejor a sus necesidades agregando el campo de estado.

export class ApiError { constructor(message, status = 500) { this.message = message; this.status = status; } }

Información Adicional

Excepciones personalizadas

Puede lanzar cualquier excepción personalizada en cualquier momento throw new Error(''whatever'') o usando una new Promise((resolve, reject) => reject(''whatever'')) . Solo tienes que jugar con Promise .

ES6 ES2017

Ese es un punto muy obstinado. IMO ES6 (o incluso ES2017 , que ahora tiene un conjunto oficial de características) es la forma adecuada de trabajar en grandes proyectos basados ​​en Node.

Si aún no lo está utilizando, intente ver las características de ES6 y ES2017 y el transpilador de Babel .

Resultado

Ese es solo el código completo (ya mostrado antes), sin comentarios ni anotaciones. Puede verificar todo lo relacionado con este código desplazándose hasta la sección correspondiente.

router.js

const controllerHandler = (promise, params) => async (req, res, next) => { const boundParams = params ? params(req, res, next) : []; try { const result = await promise(...boundParams); return res.json(result || { message: ''OK'' }); } catch (error) { return res.status(500) && next(error); } }; const c = controllerHandler; router.get(''/users/:username'', c(getUser, (req, res, next) => [req.params.username]));

controllers / user.js

import { serviceFunction } from service/user.js export async function getUser(username) { const user = await findUserByUsername(); const chat = await getChat(user); const somethingElse = doSomethingElse(chat); return somethingElse; }

services / user.js

import User from ''../models/User''; export function getUser(username) { return User.find({}).exec(); }

modelos / Usuario / index.js

import { validateUsername } from ''./validate''; const userSchema = new Schema({ username: { type: String, unique: true, validate: [{ validator: validateUsername, msg: ''Invalid username'' }], }, }, { timestamps: true }); const User = mongoose.model(''User'', userSchema); export default User;

modelos / Usuario / validate.js

export function validateUsername(username) { return true; }


regla simple y básica

  1. Mantenga los componentes asociados cerca uno del otro.

  2. Divide la página en componentes y trabajo

  3. Todos los componentes dependientes deben estar juntos.

  4. las cosas compartidas deben mantenerse independientes de todos los demás componentes.

Finalmente cada idioma es dulce. Es solo que estás familiarizado con el idioma. Solo puedes ganar la batalla si estás familiarizado con tu espada.

Estoy desarrollando la aplicación Angular2 usando NodeJS, Angular2 te ayudaré con la estructura de mi directorio.

`the main module`

`the sub module structure`

`keep the shared folder as a separate module`

Espero eso ayude :)