Создание серверной части REST API с использованием Express.js

Это Medium.com версия исходной статьи, которую я написал в LinkedIn. Вы можете увидеть это здесь: Версия для LinkedIn

Здравствуйте, меня зовут Антонио Эрделяк, я 16-летний разработчик JavaScript. Это моя первая статья и руководство.

В этой статье я буду создавать серверную часть REST API, используя Express.js узла. Я хочу подчеркнуть, что я купил курс Thinkser, чтобы узнать, как это сделать, и я буду использовать это руководство, чтобы предоставить вам бесплатную информацию и повысить свои навыки.

Поскольку это мой первый учебник, я настоятельно рекомендую хорошие знания JavaScript, поскольку сомневаюсь, что смогу хорошо объяснить, что делаю. Ожидается, что он также знает, как работать в Postman (отправлять запросы POST, PUT, DELETE и устанавливать заголовки). Также было бы неплохо иметь некоторый опыт работы с MongoDB, но это не совсем необходимо.

Загрузка нашего проекта

Мы начнем с начальной загрузки нашего проекта с помощью express-generate с небольшими изменениями. Вы можете скачать стартовый проект отсюда: Стартовый проект

После того, как вы распаковали проект, откройте его и выполните команду npm-install в указанном каталоге (где находятся app.js, config, models и т. Д.)

Давайте начнем.

1. Создание Nodemon

Начнем с применения Nodemon к нашему проекту, чтобы наш сервер автоматически обновлялся каждый раз, когда мы меняем строку кода.

В package.json внесите следующие изменения:

"scripts": {
    "start": "node ./app.js",
    //Changes
    "dev": "nodemon ./app.js",
    //Changes over
    "test": "echo \"Error: no test specified\" && exit 1"
  },

Это позволит нам запустить команду npm run dev в терминале, и Nodemon запустит наш сервер. Запустите npm run dev в терминале, чтобы запустить сервер и перейти к следующему шагу.

2. Создание модели пользователя.

Мы собираемся использовать библиотеку Mongoose, чтобы легко создавать схемы и модели для пользователей, статей, комментариев и т. Д.

В папке Модели создайте новый файл с именем User.js.

Давайте добавим несколько первых строк кода в User.js:

var mongoose = require('mongoose');
var uniqueValidator = require('mongoose-unique-validator');
var crypto = require('crypto');
var jwt = require('jsonwebtoken');
var secret = require('../config').secret;
var UserSchema = new mongoose.Schema({
    username: {type: String, unique: true, required: [true, "cannot be empty."], lowercase: true, index: true},
    email: {type: String, unique: true, required: [true, "cannot be empty."], lowercase: true, index: true},
    bio: String,
    image: String,
    salt: String,
    hash: String
}, {timestamps: true});

Отлично, мы только что создали нашу первую схему с использованием Mongoose. Мы определили имя пользователя как обязательное, уникальное и строчную строку. Мы сделали то же самое и с электронной почтой. Для других свойств мы просто установили их как Strings.

Давайте применим библиотеку uniqueValidator к нашему файлу User.js:

UserSchema.plugin(uniqueValidator, {message: "is already taken."});

Давайте добавим несколько методов в User.js

...
var UserSchema = new mongoose.Schema({
    username: {type: String, unique: true, required: [true, "cannot be empty."], lowercase: true, index: true},
    email: {type: String, unique: true, required: [true, "cannot be empty."], lowercase: true, index: true},
    bio: String,
    image: String,
    salt: String,
    hash: String
}, {timestamps: true});

UserSchema.plugin(uniqueValidator, {message: "is already taken."});

//^ OLD CODE, just so know where we left off. ^

//NEW CODE BENEATH THIS COMMENT
UserSchema.methods.setPassword = function(password){
    this.salt = crypto.randomBytes(16).toString('hex');
    this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
};

UserSchema.methods.validPassword = function(password){
    var hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
    return this.hash === hash;
};

UserSchema.methods.generateJWT = function(){
    var today = new Date();
    var exp = new Date(today);
    exp.setDate(today.getDate()+60);
    return jwt.sign({
        id: this._id,
        username: this.username,
        exp: parseInt(exp.getTime()/1000)
    }, secret)
};

UserSchema.methods.toAuthJSON = function(){
    return {
        username: this.username,
        email: this.email,
        bio: this.bio,
        image: this.image,
        token: this.generateJWT()
    };
};

Отлично, теперь мы определили следующие методы:

  1. setPassword - используется для генерации пароля путем случайного создания пользовательских свойств hash и salt для шифрования пароля, предоставленного пользователем с помощью Crypto библиотека
  2. validPassword - используется для сравнения предоставленного пароля с фактическим паролем пользователя.
  3. generateJWT - используется для создания веб-токена JSON, срок действия которого истекает через 60 дней с момента создания, и который будет храниться и использоваться в Frontend’s window.localStorage ()
  4. toAuthJSON - используется для возврата указанных свойств пользователя (имя пользователя, адрес электронной почты, биография…).

Наконец, давайте добавим UserSchema к моделям mongoose.

mongoose.model('User', UserSchema);

Теперь давайте потребуем модель User в app.js (найдите строку «app.use (require ('./ routes'));» и внесите следующие изменения) :

...
//NEW LINE OF CODE BENEATH THIS COMMENT

require('./models/User');

//OLD CODE BENEATH THIS COMMENT


app.use(require('./routes'));
/// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});
...

Отлично, теперь сервер должен зарегистрировать модель пользователя, и вы должны увидеть сообщение в терминале, которое будет выглядеть следующим образом:

Listening on port 8000
Mongoose: users.ensureIndex({ username: 1 }) { unique: true, background: true }  
Mongoose: users.ensureIndex({ email: 1 }) { unique: true, background: true }

Если у вас есть точное сообщение в терминале, поздравляю, вы закончили свою первую модель в этом уроке!

3. Создание паспорта.

Теперь мы собираемся создать паспорт, который мы будем использовать при аутентификации пользователя.

В папке config создайте новый файл с именем Passport.js со следующим кодом:

var passport = require('passport');
var LocalStrategy = require('passport-local');
var mongoose = require('mongoose');
var User = mongoose.model('User');
passport.use(new LocalStrategy({
    usernameField: 'user[email]',
    passwordField: 'user[password]'
}, function(email, password, done){
    User.findOne({email: email}).then(function(user){
        if(!user || !user.validPassword(password)){
            return done(null, false, {errors: {"email or password":"is invalid."}})
        }
        return done(null, user);
    }).catch(done);
}));

Мы будем использовать это при аутентификации данных для входа нашего пользователя, если адрес электронной почты и пароль верны, модель пользователя будет возвращена, в противном случае будет возвращена ошибка с надписью «электронная почта или пароль недействителен ».

Зарегистрируйте Passport.js в app.js:

...
require('./models/User');
//OLD Code on top, just so you can know where we left off.

//new code beneath this comment

require('./config/passport');

//old code beneath this comment
app.use(require('./routes'));

Отлично, вы создали паспорт и успешно зарегистрировали его в приложении. Проверьте терминал на наличие ошибок (их не должно быть) и переходите к следующему шагу.

4. Создание маршрута авторизации.

Мы собираемся создать файл, отвечающий за то, должен ли пользователь быть авторизован или не авторизован.

В папке routes / создайте новый файл с именем auth.js со следующим кодом:

var jwt = require('express-jwt');
var secret = require('../config').secret;
function getTokenFromHeaders(req){
    if(req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Token'){
        return req.headers.authorization.split(' ')[1];
    }
    return null;
}
var auth = {
    required: jwt({
        secret: secret,
        userProperty: 'payload',
        getToken: getTokenFromHeaders
    }),
    optional: jwt({
        secret: secret,
        userProperty: 'payload',
        credentialsRequired: false,
        getToken: getTokenFromHeaders
    })
};
module.exports = auth;

Давайте объясним, что это делает.

Функция getTokenFromHeaders проверяет наличие токена в заголовке отправленного запроса и возвращает его, если он есть, в противном случае возвращает значение null.

Объект аутентификации будет вызываться в наших маршрутах в зависимости от того, хотим ли мы, чтобы пользователю требовалось войти в систему, чтобы увидеть конкретный маршрут или выполнить с ним действие, или если нам все равно (необязательно), если он авторизован или нет.

5. Создание пользовательского маршрута.

Теперь мы собираемся создать маршрут, который будет обрабатывать регистрацию пользователя, вход в систему, обновление пользователя и т. Д.

В папке routes / api / создайте новый файл с именем users.js.

Начнем с создания возможности регистрации пользователя. Внесите следующие изменения в users.js:

var mongoose = require('mongoose');
var router = require('express').Router();
var auth = require('../auth');
var User = mongoose.model('User');
var passport = require('passport');
router.post('/users', function(req,res,next){
    var user = new User();
    user.username = req.body.user.username;
    user.email = req.body.user.email;
    user.setPassword(req.body.user.password);
    user.save().then(function(){
        return res.json({user: user.toAuthJSON()});
    }).catch(next);
});

Как видите, мы используем модель Пользователь и в настройках это свойства имя пользователя, адрес электронной почты и пароль, после чего мы сохраняем его и возвращаем. используя созданную нами функцию user.toAuthJSON ().

Теперь давайте добавим возможность входа в систему. Добавьте следующие изменения в users.js:

...
router.post('/users/login', function(req,res,next){
    if(!req.body.user.email){
        return res.status(422).json({errors: {email: "can't be blank."}});
    }
    if(!req.body.user.password){
        return res.status(422).json({errors: {password: "can't be blank."}});
    }
    passport.authenticate('local', {session: false}, function(err, user, info){
        if(err){return next(err);}
        if(user){
            user.token = user.generateJWT();
            return res.json({user: user.toAuthJSON()});
        } else {
            return res.status(422).json(info);
        }
    })(req,res,next)
});

Мы начинаем с проверки, предоставил ли пользователь адрес электронной почты и пароль, в противном случае мы немедленно возвращаем ошибку. Если все в порядке, мы используем паспорт для аутентификации пользователя, возвращаем ошибки, если таковые имеются, и, что более важно, возвращаем вошедшего в систему пользователя.

Давайте добавим возможность пользователю проверять свой профиль / модель пользователя. Добавьте следующие изменения:

router.get('/user', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        if(!user){return res.sendStatus(401);}
        return res.json({user: user.toAuthJSON()});
    }).catch(next);
});

Теперь пользователь может видеть свою информацию, но только если он вошел в систему (требуется авторизация).

Наконец, давайте добавим пользователю возможность обновлять свою информацию. Добавьте в код следующие изменения:

router.put('/user', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        if(!user){return res.sendStatus(401);}
        if(typeof req.body.user.username !== 'undefined'){
            user.username = req.body.user.username;
        }
        if(typeof req.body.user.email !== 'undefined'){
            user.email = req.body.user.email;
        }
        if(typeof req.body.user.bio !== 'undefined'){
            user.bio = req.body.user.bio;
        }
        if(typeof req.body.user.image !== 'undefined'){
            user.image = req.body.user.image;
        }
        if(typeof req.body.user.password !== 'undefined'){
            user.setPassword(req.body.user.password);
        }
        return user.save().then(function(){
            return res.json({user: user.toAuthJSON()});
        });
    }).catch(next);
});

Теперь пользователь может обновить свою информацию, если он вошел в систему. Мы также проверяем, являются ли новые обновления пользователя действительными, и оставляем без изменений те, которых нет.

Давайте создадим промежуточное ПО, которое будет показывать нам ошибки (уже отправленное письмо, неверная информация и т. Д.). Добавьте следующие изменения в users.js:

router.use(function(err,req,res,next){
    if(err.name === 'ValidationError'){
        return res.json({
            errors: Object.keys(err.errors).reduce(function(errors ,key){
                errors[key] = err.errors[key].message;
                return errors;
            }, {})
        })
    }
    return next(err);
});

Не пугайтесь этого, все, что он делает, это сортирует объект грязных ошибок как simple {errors: {email: «не может быть пустым.»}} тип объекта.

Завершите файл users.js, добавив последнюю строку кода:

module.exports = router;

Наконец, перейдите к index.js в той же папке, что и users.js, и выполните следующие действия:

var router = require('express').Router();
// existing code on top 
//new code
router.use('/', require('./users'));
//existing code beneath this comment
module.exports = router;

1. Отлично, теперь это пример того, что мы отправляем для регистрации пользователя:

С помощью Postman отправьте запрос POST со следующим телом на маршрут localhost: 8000 / api / users:

{
    "user":{
         "username":"Test",
         "email":"[email protected]",
         "password":"test"
     }
}

2. Это пример того, что мы отправляем для входа пользователя:

С помощью Postman отправьте запрос POST со следующим телом на маршрут localhost: 8000 / api / users / login:

{
    "user":{
         "email":"[email protected]",
         "password":"test"
     }
}

3. Это пример того, что мы отправляем для авторизации пользователя:

Примечание. Обязательно скопируйте токен, который вы получаете при входе или регистрации, и перейдите в настройки Заголовки Postman. Добавьте Авторизация и установите для него значение «Токен-пример» (без кавычек). Он должен выглядеть так:

Используя Postman, отправьте запрос GET на маршрут localhost: 8000 / api / user.

4. Это пример того, что мы отправляем для обновления информации вошедшего в систему пользователя:

Примечание. Обязательно скопируйте токен, который вы получаете при входе или регистрации, и перейдите в Заголовки. Добавьте Авторизацию и установите для него значение «Токен-пример».

Используя Postman, отправьте запрос PUT со следующим телом на маршрут localhost: 8000 / api / user:

{
    "user":{
         "email":"[email protected]",
         "username":"newusername"
     }
}

теперь вы должны получить нового, обновленного пользователя.

6. Создание профилей

Вернемся к models / User.js и добавим новый метод:

UserSchema.methods.toProfileJSONFor = function(user){
    return {
        username: this.username,
        bio: this.bio,
        image: this.image,
        following: false // we will change this later
    };
};

Мы будем вызывать это, чтобы получить только ту информацию, которую пользователь хотел бы, чтобы другой пользователь видел, без его токена и электронной почты.

Теперь перейдем к routes / api / и создадим новый файл с именем profiles.js.

Давайте создадим промежуточное ПО, которое будет проверять, существует ли пользователь в любое время, когда вызывается / profiles / someUser. Добавьте в profiles.js следующее:

var mongoose = require('mongoose');
var User = mongoose.model('User');
var router = require('express').Router();
var auth = require('../auth');
router.param('username', function(req,res,next,username){
    User.findOne({username: username}).then(function(user){
        if(!user){return res.sendStatus(404);}
        req.profile = user;
        return next();
    }).catch(next);
});

Чтобы объяснить, что это делает, каждый раз, когда мы запрашиваем маршрут /: username, это промежуточное ПО проверяет, существует ли это : username , а если этого не произойдет, возникнет ошибка 404 .

Давайте добавим маршрут, который вернет указанный профиль, если он существует:

router.get('/:username', auth.optional, function(req,res,next){
    if(req.payload){
        User.findById(req.payload.id).then(function(user){
            if(!user){return res.json({profile: req.profile.toProfileJSONFor(false)})}
            return res.json({profile: req.profile.toProfileJSONFor(user)});
        }).catch(next);
    } else {
        return res.json({profile: req.profile.toProfileJSONFor(false)});
    }
});

Мы проверяем, пользователь, посещающий профиль, вошел в систему или нет, и передаем зарегистрированного пользователя в toProfileJSONFor (пользователь) или не проходит, если он не вошел в систему toProfileJSONFor (false).

Не забудьте добавить последнюю строчку:

module.exports = router;

Теперь перейдите в index.js и сделайте то же самое, что и для users.js:

router.use('/profiles', require('./profiles'));

Теперь перейдите в Почтальон и отправьте запрос GET на localhost: 8000 / api / profiles / test (или localhost: 8000 / api / profiles / любое_имя_в_ базе данных)

7. Создание модели статьи.

В папке models / создайте новый файл с именем Article.js.

Начнем с создания для него схемы. Так же, как и для модели Пользователь.

var mongoose = require('mongoose');
var uniqueValidator = require('mongoose-unique-validator');
var slug = require('slug');
var User = mongoose.model('User');
var ArticleSchema = new mongoose.Schema({
    slug: {type: String, lowercase: true, unique: true},
    title: String,
    description: String,
    body: String,
    tagList:[{type: String}],
    favoritesCount: {type: Number, default: 0},
    author: {type: mongoose.Schema.Types.ObjectId, ref:'User'}
}, {timestamps: true});

Давайте добавим к нему плагин uniqueValidator

ArticleSchema.plugin(uniqueValidator, {message: "is already taken."});

Давайте реализуем метод slugify, который создаст уникальный слаг (пример: new-article-title- 7aXm9cms).

ArticleSchema.methods.slugify = function(){
    this.slug = slug(this.title) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36);
};

Давайте удостоверимся, что пуля всегда установлена.

ArticleSchema.pre('validate', function(next){
    if(!this.slug){
        this.slugify();
    }
    return next();
});

И последний метод, toJSONFor (пользователь), который вернет заголовок, заголовок, текст статьи и т. Д.

ArticleSchema.methods.toJSONFor = function(user){
    return {
        slug: this.slug,
        title: this.title,
        description: this.description,
        body: this.body,
        tagList: this.tagList,
        favoritesCount: this.favoritesCount,
        favorited: user ? user.isFavorite(this._id) : false,
        createdAt: this.createdAt,
        updatedAt: this.updatedAt,
        author: this.author.toProfileJSONFor(user)
    };
};

Не забудьте создать модель в последней строке в Article.js

mongoose.model('Article', ArticleSchema);

Наконец, перейдите в app.js и добавьте к нему модель Article.

require('./models/User');
/*New code*/ require('./models/Article');
require('./config/passport');
app.use(require('./routes'));

Большой! проверьте терминал, если есть какие-либо ошибки, убедитесь, что вы не допустили опечаток, и пытайтесь исправить их, пока не получите такое сообщение:

[nodemon] 1.11.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node ./app.js`
Listening on port 8000
Mongoose: users.ensureIndex({ username: 1 }) { unique: true, background: true }  
Mongoose: users.ensureIndex({ email: 1 }) { unique: true, background: true }

8. Маршруты статей

В routes / api / создайте новый файл с именем article.js.

Начнем с создания маршрута для создания статьи, это будет работать только если пользователь вошел в систему, потому что нам нужно , чтобы указать автора для статьи (article.author = user).

var mongoose = require('mongoose');
var router = require('express').Router();
var Article = mongoose.model('Article');
var User = mongoose.model('User');
var auth = require('../auth');
router.post('/', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        if(!user){return res.sendStatus(401);}
        var article = new Article(req.body.article);
        article.author = user;
        return article.save().then(function(){
            return res.json({article: article.toJSONFor(user)})
        });
    }).catch(next);
});

Давайте создадим промежуточное ПО, которое будет возвращать 404 ошибку, если : article не существует каждый раз, когда мы ищем : article и установите для него значение req.article it, если оно существует.

router.param('article', function(req,res,next,slug){
    Article.findOne({slug: slug})
        .populate('author')
        .then(function(article){
            if(!article){
                return res.sendStatus(404);
            }
            req.article = article;
            return next();
        }).catch(next);
});

Создание маршрута для получения указанной статьи с помощью slug

router.get('/:article', auth.optional, function(req,res,next){
    Promise.all([
        req.payload ? User.findById(req.payload.id) : null,
        req.article.populate('author').execPopulate()
    ]).then(function(results){
        var user = results[0];
        return res.json({article: req.article.toJSONFor(user)});
    }).catch(next);
});

Здесь мы используем действие Promise для Async, которое проверяет, есть ли вошедший в систему пользователь, и заполняет поле autor в req.article (статья найдена с помощью : article) , и возвращает статью, отображаемую для пользователя, или нет (если есть не авторизованный пользователь).

Теперь давайте создадим маршрут, который позволит обновить статью:

router.put('/:article', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        if(req.article.author._id.toString() === req.payload.id.toString()){
            if(typeof req.body.article.title !== 'undefined'){
                req.article.title = req.body.article.title;
            }
            if(typeof req.body.article.description !== 'undefined'){
                req.article.description = req.body.article.description;
            }
            if(typeof req.body.article.body !== 'undefined'){
                req.article.body = req.body.article.body;
            }
            return req.article.save().then(function(){
                return res.json({article: req.article.toJSONFor(user)});
            });
        } else {
            return res.sendStatus(403);
        }
    }).catch(next);
});

Здесь требуется вошедший пользователь, потому что нам нужно разрешить обновление только для автора статьи. Если пользователь не вошел в систему или пользователь не, автор, выдается ошибка 403 .

Точно так же мы собираемся создать маршрут, который позволит удалить статью:

router.delete('/:article', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        if(req.article.author._id.toString() === req.payload.id.toString()){
            req.article.remove().then(function(){
                return res.sendStatus(204);
            });
        } else {
            return res.sendStatus(403);
        }
    }).catch(next);
});

Здесь также требуется вошедший в систему пользователь по тем же причинам.

Не забудьте написать последнюю строку:

module.exports = router;

Наконец, перейдите в index.js в той же папке и добавьте article.js:

router.use('/', require('./users'));
router.use('/profiles', require('./profiles'));
/*new code*/ router.use('/articles', require('./articles'));

Выполните тестирование маршрута с помощью Postman.

9. Комментарии

Начнем с создания модели Комментарий. В models / добавьте новый файл Comment.js

var mongoose = require('mongoose');
var CommentSchema = new mongoose.Schema({
    body: String,
    author: {type: mongoose.Schema.Types.ObjectId, ref:'User'},
    article: {type: mongoose.Schema.Types.ObjectId, ref:'Article'}
}, {timestamps: true});
CommentSchema.methods.toJSONFor = function(user){
    return {
        id: this._id,
        body: this.body,
        author: this.author.toProfileJSONFor(user)
    };
};
mongoose.model('Comment', CommentSchema);

Очень простая схема для простой модели.

Не забудьте добавить Comment.js в app.js.

require('./models/User');
require('./models/Article');
/*new code*/require('./models/Comment');
require('./config/passport');
app.use(require('./routes'));

Ничего не меняйте, кроме / * нового кода * / строки.

Теперь вернемся к Article.js (Модель) и внесем некоторые изменения.

var ArticleSchema = new mongoose.Schema({
    slug: {type: String, lowercase: true, unique: true},
    title: String,
    description: String,
    body: String,
    tagList:[{type: String}],
    favoritesCount: {type: Number, default: 0},
    author: {type: mongoose.Schema.Types.ObjectId, ref:'User'},
    /*new code*/comments: [{type: mongoose.Schema.Types.ObjectId, ref:'Comment'}]
}, {timestamps: true});

Оставьте все без изменений кроме / * нового кода * / строки. Это добавляет модели комментариев в виде массива внутри статьи.

Вернемся к article.js в routes / api / article.js и добавим немного кода.

Начните с импорта модели комментария вверху.

var Comment = mongoose.model('Comment');

Теперь давайте создадим новый маршрут, который позволит нам создать комментарий к существующей статье, если мы вошли в систему.

router.post('/:article/comments', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        if(!user){return res.sendStatus(401);}
        var comment = new Comment(req.body.comment);
        comment.author = user;
        comment.article = req.article;
        comment.save();
        req.article.comments.push(comment);
        return req.article.save().then(function(){
            return res.json({comment: comment.toJSONFor(user)});
        });
    }).catch(next);
});

Здесь должны существовать и req.article, и зарегистрированный пользователь, потому что нам нужно указать автора комментария и статья.

Давайте создадим маршрут, по которому будут отображаться все комментарии в статье:

router.get('/:article/comments', auth.optional, function(req,res,next){
    Promise.resolve(req.payload ? User.findById(req.payload.id) : null).then(function(user){
        return req.article.populate({
            path: 'comments',
            populate: {
                path: 'author'
            },
            options: {
                sort: {
                    createdAt: 'desc'
                }
            }
        }).execPopulate().then(function(){
            return res.json({comments: req.article.comments.map(function(comment){
                return comment.toJSONFor(user);
            })});
        });
    }).catch(next);
});

Теперь вы уже знакомы с Promise, поэтому должно быть ясно, что это асинхронное действие, которое заполняет автора и комментария. сортирует комментарии по дате.

Давайте создадим промежуточное ПО, которое будет выдавать ошибку 404 , если : comment не существует, и установим его на req.comment, если он существует.

router.param('comment', function(req,res,next,id){
    Comment.findById(id).then(function(comment){
        if(!comment){return res.sendStatus(404);}
        req.comment = comment;
        return next();
    }).catch(next);
});

Наконец, давайте создадим маршрут, который позволит разрешить удаление комментария для автора комментария:

router.delete('/:article/comments/:comment', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        if(req.comment.author._id.toString() === req.payload.id.toString()){
            req.article.comments.remove(req.comment._id);
            return req.article.save()
                .then(Comment.findOne({_id: req.comment._id}).remove().exec())
                .then(function(){
                    return res.sendStatus(204);
                });
        } else {
            return res.sendStatus(403);
        }
    }).catch(next);
});

Проверьте маршруты комментария с помощью комментариев Postman, GET и DELETE с идентификатором.

10. Добавление статьи в избранное

Вернемся к models / User.js и внесем некоторые изменения.

var UserSchema = new mongoose.Schema({
    username: {type: String, lowercase: true, required: [true, "can't be blank."], unique: true, index: true},
    email: {type: String, lowercase: true, required: [true, "can't be blank."], unique: true, index: true},
    bio: String,
    image: String,
    salt: String,
    hash: String,
    /*new code*/favorites: [{type: mongoose.Schema.Types.ObjectId, ref:'Article'}],
}, {timestamps: true});

Ничего не редактировать, кроме / * нового кода * / строки.

Давайте добавим в модель User несколько методов для взаимодействия с недавно созданным массивом Favorites.

UserSchema.methods.favorite = function(id){
    if(this.favorites.indexOf(id) === -1){
        this.favorites.push(id);
    }
    return this.save();
};
UserSchema.methods.unfavorite = function(id){
    this.favorites.remove(id);
    return this.save();
};
UserSchema.methods.isFavorite = function(id){
    return this.favorites.some(function(favoriteId){
        return id.toString() === favoriteId.toString();
    });
};

Каждая строка говорит сама за себя, первые 2 метода (избранное и исключить) должны заканчиваться на return this.save ();

Вернемся к models / Article.js и добавим метод:

Не забудьте импортировать Модель пользователя вверху:

var User = mongoose.model('User');

Теперь вы можете использовать его для создания метода:

ArticleSchema.methods.updateFavoriteCount = function(){
    var article = this;
    return User.count({favorites: {$in: [article._id]}}).then(function(count){
        article.favoritesCount = count;
        return article.save();
    });
};

Этот метод возвращает количество людей (пользователей), у которых есть определенная статья в массиве избранного .

Измените метод toJSONFor со следующими обновлениями:

ArticleSchema.methods.toJSONFor = function(user){
    return {
        slug: this.slug,
        title: this.title,
        description: this.description,
        body: this.body,
        tagList: this.tagList,
        favoritesCount: this.favoritesCount,
        /*new code*/ favorited: user ? user.isFavorite(this._id) : false,
        createdAt: this.createdAt,
        updatedAt: this.updatedAt,
        author: this.author.toProfileJSONFor(user)
    };
};

Теперь мы можем вернуться к /routes/api/articles.js и добавить новые маршруты:

router.post('/:article/favorite', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        if(!user){return res.sendStatus(401);}
        return user.favorite(req.article._id).then(function(){
            return req.article.updateFavoriteCount().then(function(){
                return res.json({article: req.article.toJSONFor(user)});
            });
        });
    }).catch(next);
});
router.delete('/:article/favorite', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        if(!user){return res.sendStatus(401);}
        return user.unfavorite(req.article._id).then(function(){
            return req.article.updateFavoriteCount().then(function(){
                return res.json({article: req.article.toJSONFor(user)});
            })
        });
    }).catch(next);
});

Оба маршрута имеют одинаковые путь и параметр, но разный тип запроса (POST & DELETE). Эти маршруты вызывают функции, которые мы определили несколько минут назад в User.js и Article.js.

Проверьте функции добавления в избранное / удаления в избранное в Почтальоне.

11. Следующие пользователи

Вернемся к /model/User.js и внесем некоторые изменения в схему:

var UserSchema = new mongoose.Schema({
    username: {type: String, lowercase: true, required: [true, "can't be blank."], unique: true, index: true},
    email: {type: String, lowercase: true, required: [true, "can't be blank."], unique: true, index: true},
    bio: String,
    image: String,
    salt: String,
    hash: String,
    favorites: [{type: mongoose.Schema.Types.ObjectId, ref:'Article'}],
    /*new code*/following: [{type: mongoose.Schema.Types.ObjectId, ref:'User'}]
}, {timestamps: true});

Теперь давайте добавим несколько новых методов в User.js, которые будут взаимодействовать с следующим массивом.

UserSchema.methods.follow = function(id){
    if(this.following.indexOf(id) === -1){
        this.following.push(id);
    }
    return this.save();
};
UserSchema.methods.unfollow = function(id){
    this.following.remove(id);
    return this.save();
};
UserSchema.methods.isFollowing = function(id){
    return this.following.some(function(followId){
        return id.toString() === followId.toString();
    });
};

Они точно такие же, как и в методах article избранное / unavorite / isFavorite.

Перейдем в routes / api / profiles.js и добавим новые маршруты, которые будут использовать наши недавно созданные методы.

router.post('/:username/follow', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        if(!user){return res.sendStatus(401);}
        return user.follow(req.profile._id).then(function(){
            return res.json({profile: req.profile.toProfileJSONFor(user)});
        });
    }).catch(next);
});
router.delete('/:username/follow', auth.required, function(req,res,next){
    User.findById(req.payload.id).then(function(user){
        return user.unfollow(req.profile._id).then(function(){
            return res.json({profile: req.profile.toProfileJSONFor(user)});
        });
    }).catch(next);
});

Опять же, эти маршруты почти такие же, как в статье для добавления в избранное, поэтому здесь нет ничего нового.

Перейдем в models / User.js и внесем последние изменения, чтобы следующая функция работала:

UserSchema.methods.toProfileJSONFor = function(user){
    return {
        username: this.username,
        bio: this.bio,
        image: this.image,
        /* THIS LINE IS EDITED */following: user ? user.isFollowing(this._id) : false
    };
};

Edit he / * редактируется эта строка * / только выделенная часть. Все остальное оставьте без изменений.

Проверьте маршруты с помощью Почтальона.

12. Отметить маршрут

В routes / api / создайте новый файл с именем tags.js:

var mongoose = require('mongoose');
var Article = mongoose.model('Article');
var router = require('express').Router();
router.get('/', function(req,res,next){
    Article.find().distinct('tagList').then(function(tags){
        return res.json({tags: tags});
    }).catch(next);
});
module.exports = router;

Очень простой файл, который используется для создания маршрута, который будет отображать все теги, используемые в статьях.

Не забудьте добавить его в index.js в той же папке:

var router = require('express').Router();
router.use('/', require('./users'));
router.use('/profiles', require('./profiles'));
router.use('/articles', require('./articles'));
/*new code*/router.use('/tags', require('./tags'));
module.exports = router;

Ничего не меняйте, кроме / * нового кода * / строки.

Проверьте маршрут в Почтальоне (GET localhost: 8000 / api / tags) .

13. Запрос статей

В /routes/api/articles.js мы собираемся добавить новый маршрут:

router.get('/', auth.optional, function(req,res,next){
    var limit = 20;
    var offset = 0;
    var query = {};
    if(typeof req.query.limit !== 'undefined'){
        limit = req.query.limit;
    }
    if(typeof req.query.offset !== 'undefined'){
        offset = req.query.offset;
    }
    if(typeof req.query.tag !== 'undefined'){
        query.tagList = {"$in": [req.query.tag]};
    }
    Promise.all([
        req.query.author ? User.findOne({username: req.query.author}) : null,
        req.query.favorited ? User.findOne({username: req.query.favorited}) : null
    ]).then(function(results){
        var favoriter = results[1];
        var author = results[0];
        if(author){
            query.author = author._id;
        }
        if(favoriter){
            query._id = {$in: favoriter.favorites};
        } else if(req.query.favorited){
            query._id = {$in: []};
        }

        return Promise.all([
            Article.find(query)
                .limit(Number(limit))
                .skip(Number(offset))
                .sort({createdAt: 'desc'})
                .populate('author')
                .exec(),
            Article.count(query).exec(),
            req.payload ? User.findById(req.payload.id) : null
        ]).then(function(results){
            var articles = results[0];
            var articleCount = results[1];
            var user = results[2];
            return res.json({
                articles: articles.map(function(article){
                    return article.toJSONFor(user);
                }),
                articleCount: articleCount
            });
        });
    }).catch(next);
});

Не волнуйтесь, это самый длинный маршрут, который вы увидите в этом руководстве, и он совсем не такой сложный. Мы проверяем, был ли запрошен limit, offset или tag (… / article ? Limit = 10,… / article ? Offset = 2,… / article ? Tag = new) и добавьте их в запрос, если они есть.

Затем мы проверяем, был ли запрошен избранный или автор (… / article ? Favorited = user1,… / article ? Author = mark ), а также добавьте их в запрос. Наконец, мы возвращаем asyncpromise, который ограничивает заданным нами limit запросом, пропускает по смещению установленный нами запрос заполняет автора, проверяет, вошел ли в систему пользователь и выполняет его. Затем мы получаем результаты на основе наших запросов (определенных тегов, ограничений, авторов, избранных и т. д.).

Проверьте маршрут и запросы (? X = y) в почтальоне.

14. Корм ​​(последний шаг)

Снова в /routes/api/articles.js добавьте другой маршрут, но убедитесь, что это первый маршрут выше router.post ('/'…) :

router.get('/feed', auth.required, function(req,res,next){
    var limit = 20;
    var query = {};
    var offset = 0;
    if(typeof req.query.limit !== 'undefined'){
        limit = req.query.limit;
    }
    if(typeof req.query.offset !== 'undefined'){
        offset = req.query.offset;
    }
    User.findById(req.payload.id).then(function(user){
        if(!user){return res.sendStatus(401);}
        Promise.all([
            Article.find({author: {$in: user.following}})
                .limit(Number(limit))
                .skip(Number(offset))
                .populate('author')
                .exec(),
            Article.count({author: {$in: user.following}})
        ]).then(function(results){
            var articles = results[0];
            var articleCount = results[1];
            return res.json({
                articles: articles.map(function(article){
                    return article.toJSONFor(user);
                }),
                articleCount: articleCount
            });
        });
    }).catch(next);
});

Маршрут почти такой же, как и предыдущий, но на нем отображаются не все статьи, а только авторов, на которых подписан пользователь.

Проверьте маршрут в Почтальоне, сначала обязательно подписывайтесь на того, у кого есть статья!

Бэкэнд завершен!

Поздравляю! Вы закончили работу с моей серверной частью REST API, используя учебник по Express.js! Надеюсь, вы кое-что узнали, и мои инструкции не были слишком ужасными. Спасибо за ваше время, и, к вашему сведению, скоро появится только часть Frontend, касающаяся именно этого внутреннего интерфейса. :)

Антонио Эрдельжак