The starter is a copy of jobs-api final project, just with additional data.
- navigate to 06.5-jobster-api/starter
- install dependencies
npm install
- create .env and provide correct values
- you can copy from previous project (just change the DB name)
.env
MONGO_URI=
JWT_SECRET=
JWT_LIFETIME=
- start the project
npm start
- you should see "Server is listening ...." text
- delete swagger.yaml file
- remove these lines of code
- delete swagger.yaml app.js
const swaggerUI = require('swagger-ui-express');
const YAML = require('yamljs');
const swaggerDocument = YAML.load('./swagger.yaml');
app.get('/', (req, res) => {
res.send('<h1>Jobs API</h1><a href="/api-docs">Documentation</a>');
});
app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(swaggerDocument));
- remove these lines of code
app.js
const rateLimiter = require('express-rate-limit');
app.set('trust proxy', 1);
app.use(
rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
})
);
- we don't want external JS apps to access our API (only our front-end)
- remove these lines of code
const cors = require('cors');
app.use(cors());
- add "dev" script with nodemon
- change engines to current version (in my case 16)
package.json
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"engines": {
"node": "16.x"
}
- restart server with "npm run dev"
- let's explore client folder
- open client folder
- it's a react app built with CRA
- it's the same as in my React Course (JOBSTER APP), just base url points to our current server (instead of heroku app)
utils/axios.js
const customFetch = axios.create({
baseURL: '/api/v1',
});
- notice the build folder (production ready application)
- in CRA we can create build folder by running "npm run build"
- that's the one we will use for our front-end
- require "path" module
- setup express static (as first middleware) to serve static assets from client/build
- so now instead of public folder we are using client/build
app.js
const path = require('path');
app.use(express.static(path.resolve(__dirname, './client/build')));
// place as first middleware
app.use(express.json());
app.use(helmet());
app.use(cors());
app.use(xss());
- serve index.html for all routes (apart from API)
- front-end routes pick's it up from there
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/jobs', authenticateUser, jobsRouter);
// serve index.html
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, './client/build', 'index.html'));
});
app.use(notFoundMiddleware);
app.use(errorHandlerMiddleware);
- navigate to localhost:5000
- clear local storage (if necessary)
- add following properties
models/User.js
lastName: {
type: String,
trim: true,
maxlength: 20,
default: 'lastName',
},
location: {
type: String,
trim: true,
maxlength: 20,
default: 'my city',
},
- change the response structure in register and login controllers (just keep old StatusCodes)
controllers/auth.js
res.status(StatusCodes.CREATED).json({
user: {
email: user.email,
lastName: user.lastName,
location: user.location,
name: user.name,
token,
},
});
- Login and Register Works Now :):):)
- test front-end request
- in postman or front-end
- make sure email and password are the same (or change the front-end)
{
"name":"demo user",
"email":"testUser@test.com",
"password":"secret"
}
- navigate to client/src/pages/Register.js
- make sure email and password match your test user
<button
type='button'
className='btn btn-block btn-hipster'
disabled={isLoading}
onClick={() =>
dispatch(loginUser({ email: 'testUser@test.com', password: 'secret' }))}>
- import authenticateUser middleware
- setup updateUser route (protected route)
routes/auth.js
const express = require('express');
const router = express.Router();
const authenticateUser = require('../middleware/authentication');
const { register, login, updateUser } = require('../controllers/auth');
router.post('/register', register);
router.post('/login', login);
router.patch('/updateUser', authenticateUser, updateUser);
module.exports = router;
- create updateUser controller
- setup export
controllers/auth.js
const updateUser = async (req, res) => {
console.log(req.user);
console.log(req.body);
};
- complete updateUser
controllers/auth.js
const updateUser = async (req, res) => {
const { email, name, lastName, location } = req.body;
if (!email || !name || !lastName || !location) {
throw new BadRequestError('Please provide all values');
}
const user = await User.findOne({ _id: req.user.userId });
user.email = email;
user.name = name;
user.lastName = lastName;
user.location = location;
await user.save();
const token = user.createJWT();
res.status(StatusCodes.OK).json({
user: {
email: user.email,
lastName: user.lastName,
location: user.location,
name: user.name,
token,
},
});
};
- Why New Token?
- this.modifiedPaths();
UserSchema.pre('save', async function () {
if (!this.isModified('password')) return;
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
- add following properties
models/Job.js
jobType: {
type: String,
enum: ['full-time', 'part-time', 'remote', 'internship'],
default: 'full-time',
},
jobLocation: {
type: String,
default: 'my city',
required: true,
},
- default property just for fun, values will be provided by front-end
- Mockaroo
- create mock-data.json (root)
- provide test user id
- create populate.js
populate.js
require('dotenv').config();
const mockData = require('./mock-data.json');
const Job = require('./models/Job');
const connectDB = require('./db/connect');
const start = async () => {
try {
await connectDB(process.env.MONGO_URI);
await Job.create(mockData);
console.log('Success!!!');
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
start();
- latest mongoose version change
controllers/jobs.js
const getAllJobs = async (req, res) => {
const { search, status, jobType, sort } = req.query;
// protected route
const queryObject = {
createdBy: req.user.userId,
};
if (search) {
queryObject.position = { $regex: search, $options: 'i' };
}
// add stuff based on condition
if (status && status !== 'all') {
queryObject.status = status;
}
if (jobType && jobType !== 'all') {
queryObject.jobType = jobType;
}
// NO AWAIT
let result = Job.find(queryObject);
// chain sort conditions
if (sort === 'latest') {
result = result.sort('-createdAt');
}
if (sort === 'oldest') {
result = result.sort('createdAt');
}
if (sort === 'a-z') {
result = result.sort('position');
}
if (sort === 'z-a') {
result = result.sort('-position');
}
//
// setup pagination
const page = Number(req.query.page) || 1;
const limit = Number(req.query.limit) || 10;
const skip = (page - 1) * limit;
result = result.skip(skip).limit(limit);
const jobs = await result;
const totalJobs = await Job.countDocuments(queryObject);
const numOfPages = Math.ceil(totalJobs / limit);
res.status(StatusCodes.OK).json({ jobs, totalJobs, numOfPages });
};
middleware/authentication.js
const payload = jwt.verify(token, process.env.JWT_SECRET);
const testUser = payload.userId === '62eff8bcdb9af70b4155349d';
req.user = { userId: payload.userId, testUser };
- create testingUser in middleware
middleware/testUser
const { BadRequestError } = require('../errors');
const testUser = (req, res, next) => {
if (req.user.testUser) {
throw new BadRequestError('Test User. Read Only!');
}
next();
};
module.exports = testUser;
- add to auth routes (updateUser)
const express = require('express');
const router = express.Router();
const authenticateUser = require('../middleware/authentication');
const testUser = require('../middleware/testUser');
const { register, login, updateUser } = require('../controllers/auth');
router.post('/register', register);
router.post('/login', login);
router.patch('/updateUser', authenticateUser, testUser, updateUser);
module.exports = router;
- add to job routes (createJob, updateJob, deleteJob)
routes/jobs.js
const express = require('express');
const router = express.Router();
const {
createJob,
deleteJob,
getAllJobs,
updateJob,
getJob,
showStats,
} = require('../controllers/jobs');
const testUser = require('../middleware/testUser');
router.route('/').post(testUser, createJob).get(getAllJobs);
router.route('/stats').get(showStats);
router
.route('/:id')
.get(getJob)
.delete(testUser, deleteJob)
.patch(testUser, updateJob);
module.exports = router;
routes/auth.js
const express = require('express');
const router = express.Router();
const authenticateUser = require('../middleware/authentication');
const testUser = require('../middleware/testUser');
const { register, login, updateUser } = require('../controllers/auth');
const rateLimiter = require('express-rate-limit');
const apiLimiter = rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10,
message: {
msg: 'Too many requests from this IP, please try again after 15 minutes',
},
});
router.post('/register', apiLimiter, register);
router.post('/login', apiLimiter, login);
router.patch('/updateUser', authenticateUser, testUser, updateUser);
module.exports = router;
app.js
app.set('trust proxy', 1);
app.use(express.static(path.resolve(__dirname, './client/build')));
controllers/jobs
const showStats = (req, res) => {
res
.status(StatusCodes.OK)
.json({ defaultStats: {}, monthlyApplications: [] });
};
module.exports = {
createJob,
deleteJob,
getAllJobs,
updateJob,
getJob,
showStats,
};
routes/jobs.js
const express = require('express');
const router = express.Router();
const {
createJob,
deleteJob,
getAllJobs,
updateJob,
getJob,
showStats,
} = require('../controllers/jobs');
router.route('/').post(createJob).get(getAllJobs);
router.route('/stats').get(showStats);
router.route('/:id').get(getJob).delete(deleteJob).patch(updateJob);
module.exports = router;
- npm install moment
- import mongoose and moment
controllers/jobs
const mongoose = require('mongoose');
const moment = require('moment');
controllers/jobs
const showStats = async (req, res) => {
let stats = await Job.aggregate([
{ $match: { createdBy: mongoose.Types.ObjectId(req.user.userId) } },
{ $group: { _id: '$status', count: { $sum: 1 } } },
]);
stats = stats.reduce((acc, curr) => {
const { _id: title, count } = curr;
acc[title] = count;
return acc;
}, {});
const defaultStats = {
pending: stats.pending || 0,
interview: stats.interview || 0,
declined: stats.declined || 0,
};
let monthlyApplications = await Job.aggregate([
{ $match: { createdBy: mongoose.Types.ObjectId(req.user.userId) } },
{
$group: {
_id: { year: { $year: '$createdAt' }, month: { $month: '$createdAt' } },
count: { $sum: 1 },
},
},
{ $sort: { '_id.year': -1, '_id.month': -1 } },
{ $limit: 6 },
]);
monthlyApplications = monthlyApplications
.map((item) => {
const {
_id: { year, month },
count,
} = item;
const date = moment()
.month(month - 1)
.year(year)
.format('MMM Y');
return { date, count };
})
.reverse();
res.status(StatusCodes.OK).json({ defaultStats, monthlyApplications });
};
-
remove existing git repo
-
add reverse() in showStats
controllers/jobs.js
monthlyApplications = monthlyApplications
.map((item) => {
const {
_id: { year, month },
count,
} = item;
const date = moment()
.month(month - 1)
.year(year)
.format('MMM Y');
return { date, count };
})
.reverse();
- remove Procfile
- remove engines from package.json
"engines": {
"node": "16.x"
}
- fix build folder (remove /build from client/.gitignore)
- setup new github repo
- deploy to render