By using Jest library, I created
controllers/users/login.test.js
module for testing login rout. Run: npm test
In the branch cloudinary
saving avatar on the external cloud storage
cloudinary has implemented. See controllers/users/updateAvatar
for detail.
Upload image using multipart/form-data request and multer lib, resize image before saving using Jimp lib (helpers/resizeAndMoveAvatar.js)
The REST API to the example app is described below. Base url: https://rendercontacts.onrender.com
Request details
Headers:
Content-type: application-json
Body:
{
"name": "Andrii",
"email": "[email protected]",
"password": "123qweASD"
}
Response
Status: 201 Created
{
"message": "new user created",
"user": {
"email": "[email protected]",
"subscription": "starter"
}
}
Request details
This request is sent by following link from the email user received after registration.Request only need verificationToken in request string
Response
Status: 200 Ok
{
"message": "Email verification successful"
}
Request details
In response to this request, the user will be sent an email with a verification link.Headers:
Content-type: application-json
Body:
{
"email": "[email protected]"
}
Response
Status: 200 Ok
{
"message": "Email verification successful"
}
Request details
Headers:
Content-type: application-json
Body:
{
"email": "[email protected]",
"password": "123qweASD"
}
Response
Status: 200 Ok
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDdhOGYzZjZmZTAwMDc3NTE0ZjI5MiIsImlhdCI6MTY3ODQ1MjkyMSwiZXhwIjoxNjc4NTM5MzIxfQ.Mcu_xtjfrqo6-IhXRPekd_YWBIzHhst2AbZZq7c7_eg",
"user": {
"email": "[email protected]",
"subscription": "pro",
"avatarURL": "avatars/6407a8f3f6fe00077514f292.jpg"
}
}
Request details
You can use this route in your app to regain user connection in a case of the web page was closed, and valid token was saved in local Storage.
Headers:
Content-type: application-json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
Response
Status: 200 Ok
{
"name": "Dima",
"email": "[email protected]",
"subscription": "pro",
"avatarURL": "avatars/6407a8f3f6fe00077514f292.jpg"
}
Request details
Headers:
Content-type: application-json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
Body:
{
"subscription": "pro"
}
Response
Status: 200 Ok
{
"message": "Subscription set to 'pro'",
"user": {
"name": "Andrii",
"email": "[email protected]",
"subscription": "pro"
}
}
Request details
Update user avatar using multer library
Headers:
Content-type: multipart/form-data
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
FormData:
{
"avatar": {imageFile}
}
Response
Status: 200 Ok
{
"avatarURL": "http://res.cloudinary.com/dbm1pjejb/image/upload/v1678445440/rest-api-avatars/bx1yeff5ol1uas5frxyg.jpg"
}
Request details
Headers:
Content-type: application-json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
Body:
{
"name": "Dima",
"email": "[email protected]",
"phone": "+380671234567"
}
Response
Status: 201 Created
{
"message": "new contact added",
"newContact": {
"name": "Dima",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": false,
"owner": "6402f61296dea959047b4037",
"_id": "64031305e56210df7284fe18",
"createdAt": "2023-03-04T09:44:37.108Z",
"updatedAt": "2023-03-04T09:44:37.108Z"
}
}
Request details
Headers:
Content-type: application-json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
Response
Status: 200 Ok
{
"message": "Contacts list array in json format",
"contacts": [
{
"_id": "64031305e56210df7284fe18",
"name": "Dima",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": false,
"owner": {
"_id": "6402f61296dea959047b4037",
"name": "Andrii",
"email": "[email protected]"
}
},
{
"_id": "64031513e56210df7284fe1b",
"name": "Misha",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": false,
"owner": {
"_id": "6402f61296dea959047b4037",
"name": "Andrii",
"email": "[email protected]"
}
},
}
Request details
https://rendercontacts.onrender.com/api/contacts/64031305e56210df7284fe18
Headers: Content-type: application-json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
Response
Status: 200 Ok
{
"message": "Contact with Id: 64031305e56210df7284fe18",
"contact": {
"_id": "64031305e56210df7284fe18",
"name": "Dima",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": false,
"owner": "6402f61296dea959047b4037",
"createdAt": "2023-03-04T09:44:37.108Z",
"updatedAt": "2023-03-04T09:44:37.108Z"
}
}
Request details
Headers:
Content-type: application-json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
Response
Status: 200 Ok
{
"message": "contact deleted",
"result": {
"_id": "64031305e56210df7284fe18",
"name": "Dima",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": false,
"owner": "6402f61296dea959047b4037",
"createdAt": "2023-03-04T09:44:37.108Z",
"updatedAt": "2023-03-04T09:44:37.108Z"
}
}
Request details
Headers:
Content-type: application-json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
Body:
{
"name": "Anatolii2",
"email": "[email protected]",
"phone": "0673329751"
}
Response
Status: 200 Ok
{
"message": "contact with new fields",
"result": {
"_id": "6401cc29ca13c7e6fdaf4ece",
"name": "Anatolii2",
"email": "[email protected]",
"phone": "0673329751",
"favorite": false,
"owner": "6401a402df529b42f7f9223b",
"createdAt": "2023-03-03T10:30:01.809Z",
"updatedAt": "2023-03-04T12:52:23.629Z"
}
}
Request details
https://rendercontacts.onrender.com/api/contacts/64031513e56210df7284fe1b/favorite
Headers:
Content-type: application-json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
Body:
{
"favorite": "true"
}
Response
Status: 200 Ok
{
"message": "contact with new fields",
"result": {
"_id": "64031513e56210df7284fe1b",
"name": "Misha",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": true,
"owner": "6402f61296dea959047b4037",
"createdAt": "2023-03-04T09:53:23.430Z",
"updatedAt": "2023-03-04T13:21:49.759Z"
}
}
Request details
https://rendercontacts.onrender.com/api/contacts?favorite=true
Headers:
Content-type: application-json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
Response
Status: 200 Ok
{
"message": "Contacts list array in json format",
"contacts": [
{
"_id": "64031513e56210df7284fe1b",
"name": "Misha",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": true,
"owner": {
"_id": "6402f61296dea959047b4037",
"name": "Andrii",
"email": "[email protected]"
}
},
{
"_id": "6403152be56210df7284fe1e",
"name": "Vika",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": true,
"owner": {
"_id": "6402f61296dea959047b4037",
"name": "Andrii",
"email": "[email protected]"
}
},
{
"_id": "64034ac4ed91c30518073bec",
"name": "Dima",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": true,
"owner": {
"_id": "6402f61296dea959047b4037",
"name": "Andrii",
"email": "[email protected]"
}
}
]
}
Request details
https://rendercontacts.onrender.com/api/contacts?page=1&limit=2&favorite=false
Headers:
Content-type: application-json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0MDJmNjEyOTZkZWE5NTkwNDdiNDAzNyIsImlhdCI6MTY3NzkxNjk3NCwiZXhwIjoxNjc4MDAzMzc0fQ.seXRPf2_C11GkhcylP63rhgdTWJIozYrE8-K66u-beU
Response
Status: 200 Ok
{
"message": "Contacts list array in json format",
"contacts": [
{
"_id": "6403153be56210df7284fe21",
"name": "Ira",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": false,
"owner": {
"_id": "6402f61296dea959047b4037",
"name": "Andrii",
"email": "[email protected]"
}
},
{
"_id": "64034af2ed91c30518073bef",
"name": "Katya",
"email": "[email protected]",
"phone": "+380671234567",
"favorite": false,
"owner": {
"_id": "6402f61296dea959047b4037",
"name": "Andrii",
"email": "[email protected]"
}
}
]
}
-
In Compass in collection tab navigate to Index -> create new index -> choose field that should bee unique -> check Create unique index
-
In mongoose schema add prop
unique: true
to the field definition and add error handler to the schema:contactSchema.post('save', handleSchemaError);
contactSchema
const contactSchema = new Schema(
{
name: {
type: String,
required: [true, 'Set name for contact'],
unique: true,
},
email: {
type: String,
unique: true,
},
phone: {
type: String,
},
favorite: {
type: Boolean,
default: false,
},
},
{ versionKey: false, timestamps: true }
);
contactSchema.post('save', handleSchemaError);
- Create helper
handleSchemaError
which will be called while saving model with our schema. This helper callback is responsible for correct status code sending to frontend. (409 - conflict)
handleSchemaError
const isConflict = ({ name, code }) => {
return name === 'MongoServerError' && code === 11000;
};
const handleSchemaError = (error, data, next) => {
error.status = isConflict(error) ? 409 : 400;
next();
};
module.exports = handleSchemaError;