9002. Online Judge - Backend RESTful API ServerExpress, MongoDB, and Mongoose
Setup backend server for Online Judge app to host RESTful API services.
1. Project Structure
1.1 Server Files
All source files for server is under ‘./server’ folder.
1.2 Express Server
In ‘./server/server.js’, use express to create the web server.
// ./server/server.js
var express = require("express");
var favicon = require("serve-favicon");
var cookieParser = require("cookie-parser");
var path = require("path");
var bodyParser = require("body-parser");
var morgan = require("morgan");
var winston = require("./config/winston-config-rotate");
var cors = require("cors");
var passport = require("passport");
var config = require("./config/server-config");
var FileApi = require("./api/FileApi");
// Create working directory
console.log(config);
const { app: { port, cors_client_url, temp_directory } } = config;
const tempDir = path.resolve(__dirname, temp_directory);
FileApi.creatDirectory(tempDir, (err, message) => {
if (err) {
console.log(err);
} else {
console.log(message);
}
});
// Bring in the data model
require("./models/mongodb");
// Bring in the Passport config after model is defined
require("./config/passport-config");
var app = express();
app.use(favicon(path.join(__dirname, "public", "favicon.ico")));
app.use(cookieParser());
// logging
app.use(morgan("short"));
app.use(morgan("combined", { stream: winston.stream }));
// configure app to use bodyParser(), this will let us get the data from a POST
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
//app.use(cors());
app.use(function(req, res, next) {
/*
res.header(
"Access-Control-Allow-Origin",
process.env.CLIENT_WEBSITE || cors_client_url
);*/
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Methods",
"GET,HEAD,OPTIONS,POST,PUT,PATCH,DELETE"
);
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization"
);
res.header("Access-Control-Allow-Credentials", true);
res.header("preflightContinue", false);
next();
});
// Initialise Passport before using the route middleware
app.use(passport.initialize());
// routes
var routes = require("./routes");
// Use the API routes when path starts with /api
app.use("/api", routes);
// Error handling
app.use(function(err, req, res, next) {
// error level logging
winston.error(winston.combinedFormat(err, req, res));
winston.writeError(err);
//console.log(err);
if (err.name === "UnauthorizedError") {
res.status(401);
res.json({
message:
err.name + ": " + err.message ||
" You have no authorization to view this page!"
});
}
next(err, req, res, next);
});
// development error handler, will print stacktrace
if (app.get("env") === "development") {
app.use(function(err, req, res, next) {
console.log(app.get("env"));
res.status(err.status || 500);
res.json({
message: err.message,
error: err
});
});
}
// production error handler, no stacktraces leaked to user
if (app.get("env") === "production") {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.json("error", {
message: err.message,
error: {}
});
});
}
app.listen(port, () => {
console.log("Server is up and running on port number " + port);
});
1.3 Commands
In ‘package.json’, we defined three commands to start server. ‘local’ and ‘dev’ is for development, ‘stage’ is for testing.
"scripts": {
"server-local": "NODE_ENV=local nodemon ./server/server",
"server-dev": "NODE_ENV=dev nodemon ./server/server",
"server-stage": "NODE_ENV=stage nodemon ./server/server",
"local": "concurrently \"npm run client\" \"npm run server-local \"",
"dev": "concurrently \"npm run client\" \"npm run server-dev \"",
"stage": "concurrently \"npm run client\" \"npm run server-stage \"",
},
2. Express Server
2.1 Logging
Use both Morgan and Winston for logging. Read tutorial Express - Combine Morgan and Winston to learn more details.
2.2 Routing
We define the following routers for the RESTful API.
Router | Description |
---|---|
/ | api root, test if the RESTful API is working |
/authentication | sign up, login, change password |
/admin/question | manage questions(admin only) |
/admin/user | manage users(admin only) |
/admin/database | import or export data(admin only) |
/submission | submit solution |
In ‘./server/routes/index.js’, use ‘router.use()’ to separate the routers to different files.
// server/routes/index.js
var express = require("express");
var router = express.Router();
var database = require("./database");
var authentication = require("./authentication");
var question = require("./question");
var user = require("./user");
var submission = require("./submission");
var config = require("../config/server-config");
const { app: { secret } } = config;
var jwt = require("express-jwt");
var auth = jwt({
secret: secret,
userProperty: "payload" // the default name is user, changed to payload to avoid ambiguousness
});
// test route to make sure everything is working (accessed at GET http://localhost:5000/api)
router.get("/", function(req, res) {
res.json({ message: "Hello! welcome to our api!" });
});
// authentication, url: /api/authentication/login
router.use("/authentication", authentication);
// question, url: /api/admin/question
router.use("/admin/question", auth, question);
// user, url: /api/admin/user
router.use("/admin/user", auth, user);
// database, url: /api/admin/database
router.use("/admin/database", database);
// submission, url: /api/submission
router.use("/submission", submission);
module.exports = router;
3. Mongo
Use MongoDB to persist data.
3.1 Connection String
The DB connection url is defined in file ‘server/config/server-config’. We use different Mongo database instances for development, testing and deployment. For example, we can use either the local MongoDB or remote MongoDB hosted on mLab for development.
const local = {
app: app,
db: {
host: process.env.LOCAL_DB_HOST || "testuser:abc123@localhost",
port: parseInt(process.env.LOCAL_DB_PORT) || 27017,
name: process.env.LOCAL_DB_NAME || "onlinejudge"
}
};
const dev = {
app: app,
db: {
host: process.env.DEV_DB_HOST || "dev_user:abc123@ds163781.mlab.com",
port: parseInt(process.env.DEV_DB_PORT) || 63781,
name: process.env.DEV_DB_NAME || "onlinejudge_dev"
}
};
3.2 Mongoose
In ‘server/models/mongodb.js’, use mongoose to setup connection and manipulate data to MongoDB. In the ‘open’ event, create default user ‘jojozhuang/111111’ if it doesn’t exist each time when server is started.
var mongoose = require("mongoose");
const config = require("../config/server-config");
var gracefulShutdown;
// mongodb url
const { db: { host, port, name } } = config;
var dbURI = `mongodb://${host}:${port}/${name}`;
if (process.env.NODE_ENV === "production") {
dbURI = process.env.MONGOLAB_URI;
}
console.log("dbURI:", dbURI);
mongoose.connect(dbURI);
// Get collection names
mongoose.connection.on("open", function() {
const users = mongoose.connection.db.collection("users");
users.findOne({ username: "jojozhuang" }, function(err, user) {
var curDate = new Date();
if (!user) {
const defaultUser = {
username: "jojozhuang",
email: "csgeek@mail.com",
hash:
"9f51bcd7a80a8da6fa02dcc9e136cd2ea5a08a24c988e4d822ebeb0b3eb430fd9a62af4fc6e1c456cb12cbc5b8792f737166ca39b3bb0fe4d34e1cd1ae134fd3",
salt: "f8dae7c30d811b322b8763afc424fec0",
role: "admin",
timecreated: curDate
};
users.save(defaultUser, function(err) {
if (err) {
console.log("Error occurs when creating default user:" + err);
}
console.log(
"[Database Initialization] New admin user 'jojozhuang' was created!"
);
console.log("[Default Admin] User Name: jojozhuang, Password: 111111");
});
users.save(defaultUser);
} else {
console.log("[Default Admin] User Name: jojozhuang, Password: 111111");
}
});
});
// CONNECTION EVENTS
mongoose.connection.on("connected", function() {
console.log("Mongoose connected to " + dbURI);
});
mongoose.connection.on("error", function(err) {
console.log("Mongoose connection error: " + err);
});
mongoose.connection.on("disconnected", function() {
console.log("Mongoose disconnected");
});
// CAPTURE APP TERMINATION / RESTART EVENTS
// To be called when process is restarted or terminated
gracefulShutdown = function(msg, callback) {
mongoose.connection.close(function() {
console.log("Mongoose disconnected through " + msg);
callback();
});
};
// For nodemon restarts
process.once("SIGUSR2", function() {
gracefulShutdown("nodemon restart", function() {
process.kill(process.pid, "SIGUSR2");
});
});
// For app termination
process.on("SIGINT", function() {
gracefulShutdown("app termination", function() {
process.exit(0);
});
});
// For Heroku app termination
process.on("SIGTERM", function() {
gracefulShutdown("Heroku app termination", function() {
process.exit(0);
});
});
// BRING IN YOUR SCHEMAS & MODELS
require("./user");
4. Async
4.1 Sleep
sleep
var sleep = require('sleep');
sleep.sleep(5)//sleep for 5 seconds, this will block the whole event loop execution
async
const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));
const example = async () => {
console.log("About to snooze without halting the event loop...");
await snooze(5000);
console.log("done!");
};
4.2 Async Files Operations
- Use ‘ncp’ for asynchronous recursive copying file & directory.
- Use ‘app-root-path’ to access app’s root path from anywhere without resorting to relative paths like require(“../../path”).