Пишем первое REST API приложение на NodeJS (Express + Mongoose)

в 8:16, , рубрики: javascript, node.js, первый проект, Проектирование и рефакторинг, структура проекта, метки: ,

Данный топик ориентирован больше на начинающих NodeJS разработчиков. Когда я говорю о начинающем разработчике, то понимаю под этим, что некоторые знания о NodeJS у вас уже имеются. Иначе настоятельно рекомендую сначала ознакомиться с основами (например, здесь) и лишь потом вернуться к этой статье.

Так зачем же нужен этот топик, ведь в сети огромное множество материалов о том, как построить MVC приложения на основе Express? Например, вот эта статья и многие другие. Весь их недостаток для нас в том, что они описывают MVC приложения, которые по архитектуре отличаются от REST приложений. И вот тут у начинающего разработчика возникает ряд вопросов:

  • А нужны ли нам теперь view файлы?
  • А нужны ли нам теперь controllers и если да, то как они будут модифицированы?
  • Как теперь организовать структуру файлов?
  • и так далее...

Так почему же именно Express, ведь есть и другие фреймворки (Restify, например), которые созданы специально для REST приложений? Потому что именно он имеет наибольшее сообщество в сети, и вероятность столкнуться с проблемой, которую никто раньше не встречал, — крайне мала. На начальном этапе это довольно важно и спасет ваше драгоценное время. В дальнейшем никто не мешает вам начать использовать что-то более специфическое, но сейчас остановимся именно на Express.

Ну, хватит теории, приступим! Для тех, кто только начинает работать с Express или Mongoose, рекомендую ознакомиться с документацией (Express и Mongoose).

Файловая структура нашего проекта будет такова:

/config
    main.json
/handlers
    entities.js
/libs
    config.js
    crudHandlers
    mongoose.js
/models
    entities.js
/package.json
/routes.js
/server.js

package.json

{
  "name": "node-rest-express-seed",
  "version": "0.0.1",
  "dependencies": {
    "express": "~3.4.8",
    "nconf": "~0.6.9",
    "winston": "~0.7.2",
    "mongoose": "~3.8.6",
    "underscore": "~1.5.2",
    "async": "~0.2.10"
  }
}

Здесь мы описали несколько модулей, предназначение которых поясню:

  • Nconf — модуль для работы с конфигурацией.
  • Winston — модуль для работы с выводом логов.
  • Underscore — модуль с набором полезных методов для работы с обектами, массивами, коллекциями.
  • Async — модуль, помогающий избежать «callback hell» и умеещий управлять асинхронными операциями самыми разными способами.

server.js

var express = require('express');
var path = require('path');
var winston = require('winston');

var routes = require('./routes'); // Файл с роутам
var config = require('./libs/config'); // Используемая конфигурация
var db = require('./libs/mongoose'); // Файл работы с базой MongoDB

var app = express(); // Создаем обьект express

app.use(express.json()); // "Обучаем" наше приложение понимать JSON и urlencoded запросы
app.use(express.urlencoded());
app.use(express.methodOverride()); // Переопределяем PUT и DELETE запросы для работы с WEB формами

// Если произошла ошибка валидации, то отдаем 400 Bad Request
app.use(function (err, req, res, next) {
  console.log(err.name);
  if (err.name == "ValidationError") {
    res.send(400, err);
  } else {
    next(err);
  }
})

// Если же произошла иная ошибка то отдаем 500 Internal Server Error
app.use(function (err, req, res, next) {
  res.send(500, err);
});

// Инициализируем Handlers
var handlers = {
  entities: require('./handlers/entities')
}

// Метод запуска нашего сервера
function run() {
  routes.setup(app, handlers); // Связуем Handlers с Routes
  app.listen(config.get('port'), function () {
    // Сервер запущен
    winston.info("App running on port:" + config.get('port'));
  });
  db.init(path.join(__dirname, "models"), function (err, data) {
    //Выводим сообщение об успешной инициализации базы данных
    winston.info("All the models are initialized");
  });
}

if (module.parent) {
  //Если server.js запущен как модуль, то отдаем модуль с методом run
  module.exports.run = run;
} else {
  //Иначе стартуем сервер прямо сейчас
  run();
}

Код обильно задокументирован, и все должно быть понятно, кроме Handlers и Routes. О них я расскажу немного позже.

libs/config.js

// 1. Аргументы командной строки
// 2. Переменные среды
// 3. Наш собственный файл с конфигурацией
var nconf = require('nconf');
nconf.argv()
  .env()
  .file({ file: './config/main.json' });

module.exports = nconf;

И соответственно сам файл конфигурации

config/main.json

{
  "port" : 1337,
  "mongoose": {
    "uri": "mongodb://localhost/habr"
  }
}

Здесь немного настроек (пока). Мы лишь опишем, на каком порту будет крутиться наше приложение и параметры для MongoDB

Routes and Handlers

Мы ввели два новых понятия, сейчас поясню, зачем они нам нужны. Для этого необходимо вспомнить, как Express обрабатывает запросы к нашему приложению. Если говорить в общем, то Express ищет обработчик для данного API адреса, и если результат положительный — передает данные запроса в него. По сути Route — это:

app.get('/v1/blabla',someCoolFunctionForBlaBla);

Его задача пробросить данные от пользователя во внутренний обработчик — Handler. Пример такого обработчика:

var someCoolFunctionForBlaBla = function(req,res,next) {
  // Тут мы берем из объекта req все нам необходимое, например, req.query или req.body
  // Проводим все необходимые операции и вычисления
  res.send("Hello") // Отдаем данные пользователю, например, строку "Hello"
};

Routes мы будем хранить в одном файле, а вот Handlers будем хранить в других. Это даст нам разделение логики обработчиков от их интерфейсов.

routes.js

module.exports.setup = function (app, handlers) {
  app.get('/v1/entities', handlers.entities.list);
  app.get('/v1/entities/:id', handlers.entities.get);
  app.post('/v1/entities', handlers.entities.create);
  app.put('/v1/entities/:id', handlers.entities.update);
  app.delete('/v1/entities/:id', handlers.entities.remove);
};

Здесь все должно быть понятно, если нет — нужно еще раз перечитать документацию Express.

handlers/entities.js

var mongoose = require('../libs/mongoose');

// Выставляем modelName
var modelName = 'entities';
// Подгружаем стандартные методы для CRUD документов
var handlers = require('../libs/crudHandlers')(modelName);

module.exports = handlers;

Я думаю, здесь вы предполагали увидеть больше кода, чем оказалось. Поясню. Так как CRUD для сущностей базы данных часто имеет одну и ту же логику, то можно вынести это в отдельный модуль. Если же нам нужна иная логика, то можно просто переопределить методы из crudHandlers в entities.js или вовсе не использовать его.

libs/crudHandlers.js

var mongoose = require('mongoose');
var db = require('./mongoose');

module.exports = function (modelName) {

  // Список документов
  var list = function (req, res, next) {
    db.model(modelName).find({}, function (err, data) {
      if (err) next(err);
      res.send(data);
    });
  };

  // Один документ
  var get = function (req, res, next) {
    try{var id = mongoose.Types.ObjectId(req.params.id)}
    catch (e){res.send(400)}

    db.model(modelName).find({_id: id}, function (err, data) {
      if (err) next(err);
      if (data) {
        res.send(data);
      } else {
        res.send(404);
      }
    })
  };

  // Создаем документ
  var create = function (req, res, next) {

    db.model(modelName).create(req.body, function (err, data) {
      if (err) {
        next(err);
      }
      res.send(data);
    });
  };

  // Обновляем документ
  var update = function (req, res, next) {
    try{var id = mongoose.Types.ObjectId(req.params.id)}
    catch (e){res.send(400)}

    db.model(modelName).update({_id: id}, {$set: req.body}, function (err, numberAffected, data) {
      if (err) next(err);

      if (numberAffected) {
        res.send(200);
      } else {
        res.send(404);
      }

    })
  };

  // Удаляем документ
  var remove = function (req, res, next) {
    try{var id = mongoose.Types.ObjectId(req.params.id)}
    catch (e){res.send(400)}

    db.model(modelName).remove({_id: id}, function (err, data) {
      if (err) next(err);
      res.send(data ? req.params.id : 404);
    });
  };

  return {
    list  : list,
    get   : get,
    create: create,
    update: update,
    remove: remove
  }
};

Здесь мы лишь описали CRUD методы, они могут быть у вас, какие вам заблагорассудятся. Но что же такое db.model? Посмотрим подробней:

libs/mongoose.js

var mongoose = require('mongoose');
var fs = require('fs');
var path = require('path');
var async = require('async');
var config = require('./config');

mongoose.connect(config.get('mongoose:uri'));
var db = mongoose.connection;

db.on('error', function (err) {
  // Обрабатываем ошибку
});
db.once('open', function callback() {
  // Соединение прошло успешно
});

var models = {};

//Инициализируем все схемы
var init = function (modelsDirectory, callback) {
  //Считываем список файлов из modelsDirectory
  var schemaList = fs.readdirSync(modelsDirectory);
  //Создаем модели Mongoose и вызываем callback, когда все закончим
  async.eachSeries(schemaList, function (item, cb) {
    var modelName = path.basename(item, '.js');
    models[modelName] = require(path.join(modelsDirectory, modelName))(mongoose);
    cb();
  }, callback);
};

//Возвращаем уже созданные модели из списка
var model = function (modelName) {
  var name = modelName.toLowerCase();
  if (typeof models[name] == "undefined") {
    // Если модель на найдена, то создаем ошибку
    throw "Model '" + name + "' is not exist";
  }
  return models[name];
};

module.exports.init = init;
module.exports.model = model;

Для каждой сущности в БД у нас будет отдельная модель, и чтобы каждый раз, когда она нам понадобится, не делать require, нам и нужен этот файл.
Теперь осталось только описать модель entities:

models/entities.js

var path = require('path');

module.exports = function (mongoose) {

  //Объявляем схему для Mongoose
  var Schema = new mongoose.Schema({
    name: { type: String, required: true }
  });

  // Инициализируем модель с именем файла, в котором она находится
  return mongoose.model(path.basename(module.filename, '.js'), Schema);
};

Ну, вот и все, можно запускать.
Исходники всего этого — вот.

Установить с github:

git clone https://github.com/asynxis/node-rest-seed.git my-firts-app
cd my-firts-app
npm i
node server.js

После запуска вы должны увидеть такие строки:

info: All the models are initialized
info: App running on port:1337

Если этого не произошло, проверьте, запущена ли у вас MongoDB и свободен ли у вас порт 1337.

Использование

После запуска вы получаете REST сервер с такими возможностями:

  • GET /v1/entities — список
  • GET /v1/entities/:id — получаем запись с ключом id
  • POST /v1/entities — создаем (нужно только передать name в параметрах)
  • PUT /v1/entities/:id — обновляем по id (обновляемые поля также передаем в параметрах)
  • DELETE /v1/entities/:id- удаляем по id

Например, вы добавили новую сущность в БД и хотите сделать CRUD к ней. Для этого нужно проделать 4 простые вещи:

  1. Создаем файл с именем вашей новой сущности в директори handlers и определяем там modelName.
  2. Создаем файл c именем modelName в директории models и определяем там Mongoose Schema для новой сущности.
  3. Обновляем объект handlers в server.js.
  4. Добавляем нужные нам роуты в routes.js

И, вуаля, у нас есть CRUD к еще одной сущности!
Теперь можно начинать делать из этого каркаса ваше супер приложение!

P.S. Буду рад комментариям и исправлениям, сам тоже использую NodeJS недавно. Хочется помочь тем, кто также, как и я, начинает!

Автор: asynxis

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js