-
Notifications
You must be signed in to change notification settings - Fork 22
Creating a New Server
Note: This is for running the website with npm. If you are running with the sce
tool SCE-CLI, follow this backend tutorial instead.
Looking at our architecture diagram on the Our Code Structure
wiki page (link), we have different servers running in SCE's backend. "This is complicated 😭! How does it work 🤔?" you ask. The best way to learn is if you create another one on your own.
- You followed along with this simple express video
- Core-v4 is cloned and set up on your computer (see the "Getting Started" wiki page
One of the four servers (as of 12/11/21) is the logging_api (see api/logging_api
). Clicking on the link, you should see the below layout
Ignoring the Dockerfile
, we see models
, routes
and server.js
. In a nutshell:
-
models
is for defining MongoDB Schemas (if any) -
routes
is for defining Express HTTP request handlers -
server.js
runs the server itself.
Let's create our own to understand each in depth!
In the api
directory of Core-v4, create a new directory called animal_api
. This will hold all the code for your server.
SCE's backend makes use of the Mongoose library for interacting with MongoDB. Below is an example of creating said schema. For further details on schemas, check out the Mongoose docs.
Let's say we want to create a collection of different Animals. Each Animal in the collection will store information such as the Name, Description and a lifespan in years. Turning this into a schema, we get:
api/animal_api/models/Animal.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const AnimalsSchema = new Schema(
{
name: {
type: String,
required: true
},
description: {
type: String,
default: 'No description provided.'
},
lifespan: {
type: Number,
}
},
{ collection: 'Animals' }
);
module.exports = mongoose.model('Animals', AnimalsSchema);
We now need to define handlers for our MongoDB Collection to create, read, update and delete data. Create a new file called Animal.js
from a new directory called routes
, within your animal_api
directory.
At the top of your file, we need to import some predefined variables and libraries.
api/animal_api/routes/Animal.js
const express = require('express');
const router = express.Router();
const Animal = require('../models/Animal');
const {
OK,
BAD_REQUEST,
NOT_FOUND
} = require('../../util/constants').STATUS_CODES;
The above snippet imports express and the Animal schema we defined above so we can interact with the Animal collection in MongoDB.
The below handler takes a HTTP POST request and adds data from the request into MongoDB (we will test this with postman later). The data is part of req.body
and has information for each field for the Animal schema. We return the added document if the insert was successful or a 400 (BAD_REQUEST
) if not.
router.post('/createAnimal', (req, res) => {
const { lifespan } = req.body;
const numberSent = !Number.isNaN(Number(lifespan));
const newEvent = new Animal({
name: req.body.name,
description: req.body.description,
lifespan: numberSent ? Number(lifespan) : undefined,
});
Animal.create(newEvent)
.then((post) => {
return res.json(post);
})
.catch(
(error) => res.sendStatus(BAD_REQUEST)
);
});
The below handler takes an HTTP GET request and returns all documents in the Animal collection.
router.get('/getAnimals', (req, res) => {
Animal.find()
.then(items => res.status(OK).send(items))
.catch(error => {
res.sendStatus(BAD_REQUEST);
});
});
This handler is similar to the createAnimal
above, but instead of creating a document, it uses the ID of an existing document to update its data. The parameters sent in the request are extracted in the first lines of the function and are used to optionally update each of the document's fields.
router.post('/editAnimal', (req, res) => {
const {
name,
description,
lifespan,
_id,
} = req.body;
Animal.findOne({ _id })
.then(Animal => {
Animal.name = name || Animal.name;
Animal.description = description || Animal.description;
Animal.lifespan = lifespan || Animal.lifespan;
Animal
.save()
.then(() => {
res.sendStatus(OK);
})
.catch(() => {
res.sendStatus(BAD_REQUEST);
});
})
.catch(() => {
res.sendStatus(NOT_FOUND);
});
});
Below is a function that takes an HTTP post request. It expects one parameter in the JSON body which is the MongoDB ID of the document that is to be deleted.
router.post('/deleteAnimal', (req, res) => {
Animal.deleteOne({ _id: req.body._id })
.then(result => {
if (result.n < 1) {
res.sendStatus(NOT_FOUND);
} else {
res.sendStatus(OK);
}
})
.catch(() => {
res.sendStatus(BAD_REQUEST);
});
});
Note: Be sure to add module.exports = router
to the bottom of your Dessert file in routes!
Before testing our new API, we need to create the server class. This will import the handlers we defined above and attach them to a server listening on a specified port. The below snippet handles such case.
api/animal_api/server.js
const { SceHttpServer } = require('../util/SceHttpServer');
function main() {
const API_ENDPOINTS = [
__dirname + '/routes/Animal.js',
];
const animalServer = new SceHttpServer(API_ENDPOINTS, 8084, '/animal_api/');
animalServer.initializeEndpoints().then(() => {
animalServer.openConnection();
});
}
main();
We see 3 parameters being sent to SceHttpServer
, below is a short explanation of each:
-
API_ENDPOINTS
points to where we defined our request handlers -
8084
is the port we want to listen -
/animal_api/
is the base of our request endpoints.
This server takes those parameters and generates request URLs following the format of
http://localhost:<port>/<base url>/<API handler file name>/<handler>
So in the case of our Animal example, the servers handler URLs become:
http://localhost:8084/animal_api/Animal/createAnimal
http://localhost:8084/animal_api/Animal/getAnimals
http://localhost:8084/animal_api/Animal/editAnimal
http://localhost:8084/animal_api/Animal/deleteAnimal
In api/devServer.js
, add a require
statement to add your Animal server. The file then becomes:
// This if statement checks if the module was require()'d or if it was run
// by node server.js. If we are not requiring it and are running it from the
// command line, we create a server instance and start listening for requests.
if (typeof module !== 'undefined' && !module.parent) {
// Starting servers
require('./main_endpoints/server');
require('./cloud_api/server');
require('./peripheral_api/server');
require('./animal_api/server'); // add this line!
}
Postman allows us to send HTTP requests in a sandbox environment to our APIs (here is the download page.
Before testing, your animal_api
directory should now have a layout like:
animal_api/
├── models/
│ └── Animal.js
├── routes/
│ └── Animal.js
└── server.js
Let's add a Dog to our database. Call our create handler with a JSON body of
{
"name": "Dog"
}
Below is the result. Our code should insert the data and return the created MongoDB document. Notice our description default value worked and since we didn't add a lifespan, the document doesn't have the field!
Try adding another animal with a description and lifespan and compare the results.
Let's call our read handler to get all of our Animals in MongoDB. We should get an array of documents like below:
Below we update the description of a document, passing in its MongoDB ID.
Rerun the read handler above to ensure the data updated.
Below deletes a document by its MongoDB ID. Rerun the read handler above to ensure the data updated.