Skip to content

Creating a New Server

Neel Patel edited this page Jun 24, 2022 · 17 revisions

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.

Before starting this tutorial

  • You followed along with this simple express video
  • Core-v4 is cloned and set up on your computer (see the "Getting Started" wiki page

General Layout of an SCE Server

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

image

Explanation of each compoment

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!

Creating a New Directory for Your Server

In the api directory of Core-v4, create a new directory called animal_api. This will hold all the code for your server.

Creating Your Own Model

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);

Defining HTTP Request Handlers

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.

Imports

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.

Creating Data

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, (error, post) => {
    if (error) {
      return res.sendStatus(BAD_REQUEST);
    } else {
      return res.json(post);
    }
  });
});

Reading Data

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);
    });
});

Updating Data

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);
    });
});

Deleting Data

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);
    });
});

Defining a Server Class

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

Registering Server to the SCE Backend

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!
}

Testing With Postman

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

Creating a Document

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!

image

Try adding another animal with a description and lifespan and compare the results.

Reading all Documents

Let's call our read handler to get all of our Animals in MongoDB. We should get an array of documents like below: image

Updating a Document

Below we update the description of a document, passing in its MongoDB ID. image

Rerun the read handler above to ensure the data updated.

Deleting a Document

Below deletes a document by its MongoDB ID. Rerun the read handler above to ensure the data updated.

image

Clone this wiki locally