Assignment 3.2: Generic Social Media Backend
Written by Michael, with help and feedback from Kashif Nazir. Inspired by the Flutterer assignment from CS106AXDue Sat May 27 11:59pm PT
Submissions not accepted after Mon May 29 11:59pm PT.
Backstory: Whoops! The person who wrote the backend for the app just ghosted [vague acquaintance]. They left the server running, but y'all don't have access to the code, so [vague acquaintance] can't add their cool new feature. Guess you'll have to rewrite the API from scratch first. (Your acquaintance says they're too busy working on the five year feature roadmap to do it themself... Something about this division of labor and compensation scheme feels off, but no time to think about that right now, gotta keep moving!)
This assignment closes the loop on assignment 3.1 by having you implement the API backend for the Generic Social Media App.
Learning goals
Through this assignment, you will
- Implement a REST API using Node and Express,
- Store and retrieve data in a MongoDB database,
- Work with an API specification from an implementer's perspective, and
- Integrate and test an API with an existing frontend.
Overview
Before starting this assignment, you will need to install MongoDB. Follow the instructions on that page, which include steps to verify your installation is successful.
Then download the starter code and run npm install
as usual. You will implement the API in api/index.js
.
You don't need a fully working assignment 3.1 to work on this assignment, but it may be helpful for end-to-end testing. To use your assignment 3.1 code, copy all the files from the public/js
folder in your assign3.1
into this assignment's public/js
folder. Then, uncomment the line in apirequest.js
to set API_URL
to "/api"
. This will have your apiRequest
function use the API you write for this assignment.
We have provided a small page that you can use to make API requests. When you go to localhost:1930
, you will see links to the API tester (HTML in test_api.html
) and the app (HTML in social.html
). The API tester works like this:
- There is a dropdown list of the API's endpoints.
- If the endpoint has a route parameter (id) or query string parameter (target), there will be an input to enter its value.
- If the endpoint needs a request body, there is a textarea to enter it in JSON. You will need to write well-formed JSON here (you can use
JSON.stringify
in the console). If you leave this blank, no request body will be included. - There are two checkboxes and corresponding textareas:
- Local refers to the API you are writing for this assignment.
- Remote refers to the API you used in assignment 3.1. 00
- When you click Send, the API request is sent to each API that is checked, and the response is shown in the textarea:
- If the server responds with valid JSON, it will be shown, with the HTTP status.
- If there is a problem connecting to the server, or the response isn't JSON, a general error message will be shown. You will need to look in the console or network tab to get details of what went wrong.
Before using the API tester, open js/test_api.js
and fill in REMOTE_API_URL
with your API URL from assignment 3.1. This will allow you to compare the structure of your API responses with ours. You may also find it useful to review the code in _doReq
to confirm how requests are formed.
Database structure
You will store the data for your app in a MongoDB database. In your code, you must use the DATABASE_NAME
variable to select the database to store and retrieve data. This is set up so we can point your API at a different database for grading. The default database name is set to cs193x_assign3
.
The database contains two collections: users and posts. The documents in each collection have the following fields:
users
id
: the user's ID. This should always be unique. This won't be enforced by MongoDB, so you should be careful not to store two user documents with the sameid
.name
: the user's display name.avatarURL
: the user's avatar URL.following
: an array of the users this user is following. Each element of the array is a user ID. The IDs in the array should be unique (again, not enforced by MongoDB).
posts
userId
: the user ID of the poster.time
: the date/time the user made the post. This is a JavaScriptDate
object.text
: the text of the post.
Initialization
To initialize (or reset) the database, run mongosh init_db.mongodb
from your terminal. (On Windows, replace mongosh
with the full path to mongosh.exe
, e.g. "PATH_WHERE_YOU_EXTRACTED_THE_ZIP_FILE\bin\mongosh.exe"
; use tab completion to enter the path more easily.)
After running this command, your database will look like this:
> use cs193x_assign3
switched to db cs193x_assign3
> db.users.find()
{ "_id" : ObjectId(<...>), "id" : "mchang", "name" : "Michael", "avatarURL" : "images/stanford.png", "following" : [ ] }
> db.posts.find()
{ "_id" : ObjectId(<...>), "userId" : "mchang", "time" : ISODate(<...>), "text" : "Welcome to the Generic Social Media App!" }
API description
You are implementing the same API as you used in assignment 3.1. A few assignment 3.2 specific details:
- All API endpoints are under the
/api
prefix. - You can assume the request body, if present, will be a well-formed JSON object. That is, you don't have to worry about
body-parser
failing to parse the request body. - You are expected to generate all of the errors listed in the specification. For each, you should set the HTTP status according to the spec, and your response should be an object that includes an
error
key with a message describing the error. Your messages should be human-readable and descriptive. They don't need to match ours exactly. - You do not need to edit the
/
route to match the example in the spec. - You do not need to handle any concurrency issues, e.g. getting two requests at the same time to create the same user.
Here are a few tips for implementing these endpoints:
- If you don't get the "Server started" message when you run
npm start
, this likely means your MongoDB server isn't running, so your backend can't connect to it. - The order you define endpoints can matter; Express checks for a match from top to bottom. For example, you need to put all your endpoints above the
api.all("/*")
"catch-all" endpoint, or your handler will never be called. - Pay close attention to the structure of the response in the spec (and the remote API in the tester). E.g. returning an array is not the same as returning an object with one key/value pair, where the value is an array. If your assign3.1 app works fine with our API but gets "Cannot read properties of undefined" or similar errors with yours, this is a likely cause.
- When returning data to the client, don't forget to remove (or not include) key/value pairs that aren't in the spec, e.g. the internal
_id
for each document. - Use
new Date()
to get aDate
object that represents the current date and time. You can directly return (viares.json
) aDate
object, and it will be formatted correctly. - For the feed endpoint, you'll need to do some work to build the return value, because the
posts
collection only contains the poster's user ID. This is done so that older posts won't end up with stale values if the poster changes their display name or avatar. - The logic for the feed endpoint can be a little complex. A clean approach would be to look up only the users and posts that you need, using MongoDB's $in query operator. Alternatively, you can just get all the users and posts and filter them in JavaScript; that will work fine for our needs.
- You will need to sort posts using a comparison function. See the description of how this works in the documentation for sort. You can compare (or subtract)
Date
objects as if they are numbers. - We hope many of the endpoints are relatively direct applications of the material (and code) covered in the Node/backend lectures. The feed endpoint is a bit more of a step up from that, incorporating a bit more JavaScript logic. You don't need to use any fancy tricks or make the code as efficient as possible.
Submitting
When you are finished, please submit your assign3.2
folder (without node_modules
) to Paperless.