--- layout: page title: Backend 2 - Kahoot API published: false comment_term: placeholder --- <!-- ![](img/enm.jpg){: .small } --> ## Overview In this assignment we will augment our frontend knowledge of creating beutiful websites by learning how to design backends, the designs that provide your sites with data and handle complex logic. You will build a replica of the API for the quiz website [Kahoot](https://kahoot.com/). The assignment will help you practice more complex database and API design techniques and help you learn how to test a backend without using a frontend. <!-- Perhaps some video of the final product could go here --> ## Setup ### Database (Mongo) First, you need to install mongodb, the database client for this api. On OSX something like this should work: ```bash brew uninstall --force mongodb brew tap mongodb/brew brew install mongodb-community ``` ### Pull Starter Now, go ahead and pull the backend starterpack that contains the pacakges and boilerplate needed for a express and mongo backend. We're going to be building an API, where users can make requests to create or respond to quizzes. We will be using [a backend starterpack](https://github.com/dartmouth-cs52/express-babel-starter) to start, which sets us up with an `express` node server with a tiny bit of boilerplate as well as linting and babel. You could easily recreate this, but for now we'll save you some time by providing it for you. πŸš€ This is similar to cloning a repo with `git clone`, but with a key difference. Instead of cloning from one remote location, we will add a new "remote" (basically a reference to a repo hosted on Github) so that we can pull code from there but also retain the reference to our repo for this project. That way, we don't modify the starterpack when we want to push our changes to Github. ```bash # make sure you are in your project directory git remote add starter STARTER_REPO_URL git pull starter main ``` Then run these following commands in your project directory to start our new node+express app in dev reloading mode. ```bash npm install npm start ``` ## Intro Express [Express](https://expressjs.com/) is a **server side** web framework for Node.js. What it does for us is provide a way to listen for and respond to incoming web requests. Today, we will be creating API endpoints to respond to certain CRUD-style requests. Recall that the `src/server.js` file is the entry point for our API. Note how we are setting the route: ```javascript // default index route app.get('/', (req, res) => { res.send('hi'); }); ``` The 2nd parameter to `.get()` is a function that takes 2 arguments: `request` and `response`. `request` is an express object that contains, among other things, any data that was part of the request. For instance, the JSON parameters we would POST or PUT in our asynchronous `axios` calls would be available as `req.body.parameterName`. `response` is another special express object that contains, among other things, a method named `send` that allows us a send back a response to the client. When your API call gets back JSON data this is how it is returned. Consider `res.send()` the equivalent of a network based `return` statement. This is important. You can only have **1** `res.send()`. Note that it is good practice to `return res.send()` unless you intend to run code after sending the response. We'll add more routing in shortly, but first let's set up our database! ## Mongo Database Server We will need a database to store the information aboout the kahoot quizzes and the responses from players. For this version of the assignment, we will be using the non-relational [MongoDB](https://www.mongodb.com/) as our database. We've already installed `mongodb` using Homebrew. In assignment ~~NNN~~, you will replicate the API with [PostgreSQL](https://www.postgresql.org/), a relational database. πŸš€ You may need to run the `brew services start mongodb-community` process, which your node app will connect to. This is a background server process. Recall you can interface with your database from the commandline using the mongo shell with the command `mongo`. You can also play around with a more graphical client [mongodb compass community](https://www.mongodb.com/download-center?jmp=nav#compass) (just make sure to download the *community* version). ## Mongoose <!-- ![](img/mongoose.jpg){: .small .fancy } --> To interface with mongo for our API, we will use a module called `mongoose`. [Mongoose](http://mongoosejs.com/) is a an object model for mongo. This allows us to treat data that we are storing in mongo as objects that have a nice API for querying, saving, validating, etc. rather than writing Mongo queries directly. Mongo is in general considered a schema-less store. We store JSON documents in a large object tree similarly to firebase. However, with Mongoose we are able to specify a schema for our objects. This is purely in code and allows use to validate and assert our data before inserting it into the database to protect us from pesky bugs. πŸš€ Install mongoose: `npm install mongoose` πŸš€ Add this snippet to get mongoose initialized with our database at the bottom of `server.js`. We will wrap this in an anonymous `async` function so that we can `await` our call to connect to the database: ```javascript import mongoose from 'mongoose'; // DB Setup (async () => { // connect mongo const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost/kahootAPI'; await mongoose.connect(mongoURI); // start listening app.listen(port, () => { console.log(`Listening on port ${port}`); }); })(); ``` ## Models We're going to create a data model to work with. A data model in mongoose is initialized from a schema, which is a description of the structure of the object. This is much more like what you might be familiar with statically typed classes in Java. πŸš€ Create a directory `src/models` and a file inside this directory named `room_model.js`. This is very similar to the express-mongo assignment. ```javascript import mongoose, { Schema } from 'mongoose'; // schema const RoomSchema = new Schema({ creator: String, questions: [String], answers: [String], submissions: [], }, { toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true, }); // class const RoomModel = mongoose.model('Room', RoomSchema); export default RoomModel; ``` Take a look at the `submissions` field of the `Room` model above. We know that the `questions` and `answers` fields should both be the same length, since each answer corresponds to one question. We could validate that these two fields have the same number of elements when a user tries to create a game, but why not combine these two fields into an array of nested documents. Our model will look a lot cleaner, and we will get the validation for free! We also know that the submissions to a room will be an array, in this case an array of responses from various different players. We should also check that the submissions follow a consistent format. Otherwise, players could submit more answers than the number of questions or submit the wrong type of data in response to the questions. This might cause problems when we later try to evaluate the submissions. To solve this issue, also create a file `submission_model.js` that contains a `SubmissionModel`. This represents a submission (a series of answers to the room's questions) by one player. The submission will indicate the player who submitted the response and the responses themselves. The `user` field is a username string (attached to that room) that we let them define. Right now, there could be duplicates in a given room. EC: What type of validation might we want on usernames? What would we have to do if we wanted usernames to persist across rooms? While including the the `SubmissionSchema` directly in the `RoomSchema` would work just fine, it would also mean that all the submissions would be stored inside with the rooms that they are for. For the purpose of this assignment, we are going to take a different approach, using mongoose's `populate()` function to retrieve the submission information only when we need it and storing the submissions in a separate collection. This will become more clear once we write the controllers for rooms and submissions. Write your `SubmissionSchema` to reflect this change: ```javascript const SubmissionSchema = new Schema({ user: { type: String, required: true, }, responses: [String], }, { toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true, }); ``` We hope you do not leave this assignment thinking that there are not alternative approaches. We could have the submissions stored inside of the room document or even have the rooms not store any information about which submissions they own and instead have the *submissions* know the rooms to which they belong. All of these are valid, but here we chose to take the approach of storing references to the submissions inside the room document, but keeping the bulk of the information in a separate collection. Now, let's update the `RoomSchema` to include our new combined question/answer format and submission schema. ```javascript import mongoose, { Schema } from 'mongoose'; // schema const RoomSchema = new Schema({ creator: String, questions: [{ prompt: String, answer: String }], submissions: [{ type: Schema.Types.ObjectId, ref: 'Submission', }], }, { toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true, }); // class const RoomModel = mongoose.model('Room', RoomSchema); export default RoomModel; ``` Finally, let's add a *virtual* to our room schema to compute the "scoreboard" to compare how players are performing in a given game. Add the following code to the bottom of `room_model.js`: ```javascript function generateScoreboard() { const scoreboard = this.submissions.map((submission) => { let numCorrect = 0; submission.responses.forEach((response, index) => { if (response === this.questions[index].answer) { numCorrect += 1; } }); return { Player: `${submission.player}`, 'Correct Answers': `${numCorrect}` }; }); // sort the scoreboard by decreasing number of correct answers scoreboard.sort().reverse(); return scoreboard; } RoomSchema.virtual('scoreboard').get(generateScoreboard); ``` ## Controllers πŸš€ Create a directory `src/controllers` and a file inside this named `room_controller.js`. This controller will be responsible for handling all the requests related to rooms (aka games) for our API: ```javascript export const createRoom = (roomInitInfo) => { }; export const deleteRoom = (roomID) => { }; // for room creators checking the status of the game or room properties export const getAllRoomInfo = (roomID) => { }; // for players checking the status of the game export const getLimitedRoomInfo = () => { }; export const getRoomInfo = (roomID) => { }; export const getScoreboard = async (roomID) => { }; ``` Also add a file `submission_controller.js` that will handle submissions-related requests. ```javascript export const submit = async (roomID, user, responses) => { }; ``` These are empty right now, but we will come back to them soon. ## Routing Before we finish the controllers, let's define our routes. Each route defines a url that a client of the api can hit to either retrieve data or send new data to be stored. Our api will listen to requests at these routes and respond accordingly, usually by calling one of the controller methods we defined above. In the first assignment, we put our routes in `src/server.js`, now we'll move them out to a new file, `src/routes.js`: ```javascript // default index route router.route('/rooms') .get(async (req, res) => { try { const rooms = await Rooms.getAllRooms(); return res.json(rooms); } catch (err) { return res.status(500).json(`${err}`); } }) .post(async (req, res) => { const roomInitInfo = req.body; try { const result = await Rooms.createRoom(roomInitInfo); return res.json(`Created room with id: ${result.id}`); } catch (err) { return res.status(500).json({ err }); } }); router.route('/rooms/:id') .get(async (req, res) => { const roomID = req.params.id; try { const roomInfo = await Rooms.getRoomInfo(roomID); return res.json(roomInfo); } catch (err) { return res.status(404).send(`ERROR: ${err}`); } }) .delete(async (req, res) => { const roomID = req.params.id; try { await Rooms.deleteRoom(roomID); return res.json({ message: `Room ${roomID} deleted successfully` }); } catch (err) { return res.status(500).json({ err }); } }); router.get('/rooms/:id/scoreboard', async (req, res) => { const roomID = req.params.id; try { const scoreboard = await Rooms.getScoreboard(roomID); return res.json(scoreboard); } catch { return res.status(404).json({ message: 'Room not found' }); } }); router.post('/rooms/:id/submission', async (req, res) => { const roomID = req.params.id; const { player, responses } = req.body; try { await Rooms.submit(roomID, player, responses); return res.json({ message: 'Submitted successfully' }); } catch (err) { return res.status(500).json(`${err}`); } }); export default router; ``` Recall that, when we reference `req.params.id`, the `:id` defined in the route. When the user makes a GET request to the route `rooms/33`, for example, `req.params.id` is 33. πŸš€ Running into trouble? Don't forget to import your Room and Submission controller functions: ```javascript import * as Rooms from './controllers/room_controller'; import * as Submissions from './controllers/submission_controller'; ``` ## Back to Controllers Now we'll finish the controllers for both the submissions and rooms. ### createRoom We should first implement the `createRoom` endpoint. Without any rooms, we can't play! πŸš€ We use the `new` keyword to initialize a new room, then give it the appropriate properties and save it using the `.save()` method: ```javascript export const createRoom = (roomInitInfo) => { const newRoom = new Room(); newRoom.creator = roomInitInfo.creator; newRoom.questions = roomInitInfo.questions; newRoom.answers = roomInitInfo.answers; newRoom.submissions = []; return newRoom.save(); }; ``` The `.save()` method returns a promise, so we can `await` this method when we call it. πŸš€ Let's see if this works! For this assignment, we do not have a frontend, so we will instead use [Postman](https://www.postman.com/) to test our API. Postman is a tool that lets us make requests to an api. You can download Postman as a desktop app or just use the web interface by creating an account and navigating to web.postman.co. Start up your api and, using Postman, test that creating a room via this endpoint returns a success message. ### deleteRoom πŸš€The complement to creating a room is deleting it. Write the `deleteRoom` method to handle this. ```js export const deleteRoom = (roomID) => { return Room.findByIdAndDelete(roomID); }; ``` ### getRoomInfo We need an endpoint for two groups to be able to access the information about a room: the room creator/administrator and the players participating in that room. The issue is that these groups have different privileges in terms of what they should be allowed to know about a room. The creator should obviously be able to see all of the associated data to make sure they have set up the room correctly or to see what participants have submitted. The players, however, should only be able to see the questions (so they can submit answers to them!) and the scoreboard (to see how they are doing). You can imagine the game would be pretty boring if players got the answers alongside the questions. Therefore, we need some way to return different information to these two different parties. We could have two different endpoints, but there is a risk that the route for the "creator" endpoint is accidentally exposed, which would lead to cheating. Instead, we can have an optional query parameter that is a "password" to access the extra, protected information about the room. The creator will specify the parameter when creating the room and include it in subsequent requests to this endpoint. Depending if a request to GET `rooms/:id` has a password (and it is correct), we can pass the request to two different controller methods. To support this behavior, we will need to modify our `rooms/:id` route: ```js const roomID = req.params.id; const { roomKey } = req.query; if (roomKey) { try { const room = await Rooms.getAllRoomInfo(roomID, roomKey); return res.json(room); } catch (err) { return res.status(500).json({ err }); } } else { try { const roomInfo = await Rooms.getLimitedRoomInfo(roomID); return res.json(roomInfo); } catch (err) { return res.status(500).json({ err }); } } ``` We will obviously need to split `getRoomInfo` into `getAllRoomInfo` and `getLimitedRoomInfo` as well. Finally, we will need to modify our model and ## Deploy to Render? ## MongoDB Atlas Wait, but we don't have a database on our remote server! The problem is that Heroku does not support easy storage, there is no "hard drive" to save a database file on for instance. Every Heroku process (what runs your code every time you push), is called a Dyno - and Dynos don't get their own filesystems. They get what Heroku calls an [ephemeral filesystem](https://devcenter.heroku.com/articles/dynos#ephemeral-filesystem), more of a temporary scratchpad. To run a mongoDB process with remote access there are several options, but we'll choose the cloud mongo option offered by mongodb.com. 1. Create an account at [cloud.mongodb.co](https://www.mongodb.com/cloud/atlas/signup) 1. Select the free *Shared Clusters*. 1. Pick most the defaults, in particular under *Cluster Tier* Select the `M0 Sandbox`(which is free). Don't turn on backups as that will add cost. 1. This will create a "Project 0" with "Cluster 0". You are limited to 1 free cluster per project, so later on you may want to create more. For now it is probably fine to use the same database for all your projects. 1. Create an access username and password. Save them. ![](img/newuser0.jpg){: .medium .fancy} ![](img/newuser.jpg){: .medium .fancy} 1. In *Network Access*, select *Allow Access From Anywhere* ![](img/network.jpg){: .medium .fancy} 1. Click *Clusters* -> *Connect* -> *Connect Your Application* 1. Copy the connection string into a safe place and replace `<password>` with the password you saved earlier. ## Connect Heroku to Mongo 1. Now you need to connect to a mongo database. Go to [dashboard.heroku.com](https://dashboard.heroku.com). 1. Go to *Settings* -> *Reveal Config Vars* This is where you can add environment variables β€” a great place for things like api keys and connection strings. 1. Add a key `MONGODB_URI` and paste the connection string you saved above into it. Remember to replace `<password>` with the actual password. ## Testing! We will test your api using Curl, a CLI for making web requests.