Build your own not-quite-Twitter!
This starter code implements users (with login/sessions), and freets so that you may focus on implementing your own design ideas.
The project is structured as follows:
index.tssets up the database connection and the Express server/freetcontains files related to freet conceptcollection.tscontains freet collection class to wrap around MongoDB databasemiddleware.tscontains freet middlewaremodel.tscontains definition of freet datatyperouter.tscontains backend freet routesutil.tscontains freet utility functions for transforming data returned to the client
/usercontains files related to user conceptcollection.tscontains user collection class to wrap around MongoDB databasemiddleware.tscontains user middlewaremodel.ts- contains definition of user datatyperouter.ts- contains backend user routesutil.tscontains user utility functions for transforming data returned to the client
/publiccontains the code for the frontend (HTML/CSS/browser JS)
Make a copy of this repository under your personal GitHub account by clicking the Use this template button. Make sure to enable the Include all branches option.
If you did not take 6.031 in Fall 2021 or Spring 2022, to ensure that your machine has the necessary software for the assignment, please follow Steps 1, 2, 5, and 6 on this page from the 6.031 website (now 6.1020).
- Navigate to the root folder of your cloned repository.
- Run
source demo-setup.shto set up the demo branches. - Check your local branches with
git branch; you should have one new branch, with a new commit.view-demodemos how to extend functionality of a resource
- If everything looks good, run
git push --all origin. At this point, you should see the demo branch athttps://github.com/<username>/<repo-name>/branches(and theview-demo-codebranch can now be deleted!) - Now, if you navigate to the commit history of this branch (
https://github.com/<username>/<repo-name>/commits/<branch-name>), you can click on the "demo:" commit and see exactly what we changed for each demo!
Follow the instructions here to add your fritter project to MongoDB Atlas.
After following the instructions above, you should have copied a secret that looks something like mongodb+srv://xxxxxx:xxxxxxxxx@cluster0.yc2imit.mongodb.net/?retryWrites=true&w=majority. Note that this allows complete access to your database, so do not include it anywhere that is pushed to GitHub or any other publicly accessible location.
To allow your local server to connect to the database you just created, create a file named .env in the project's root directory with the contents
MONGO_SRV=mongodb+srv://xxxxxx:xxxxxxxxx@cluster0.yc2imit.mongodb.net/?retryWrites=true&w=majority
where the secret is replaced with the one you obtained.
After finishing setup, we recommend testing both locally running the starter code, and deploying the code to Vercel, to make sure that both work before you run into issues later. The instructions can be found in the following two sections.
Firstly, open the terminal or command prompt and cd to the directory for this assignment. Before you make any changes, run the command npm install to install all the packages in package.json. You do not need to run this command every time you make any changes, unless you add a new package to the dependencies in package.json.
Finally, to test your changes locally, run the command npm start in the terminal or command prompt and navigate to localhost:3000 and interact with the frontend to test your routes.
We will be using Vercel to host a publicly accessible deployment of your application.
-
Create a fork of this repository through GitHub. This will create a repository in your GitHub account with a copy of the starter code. You'll use this copy to push your work and to deploy from.
-
Create a Vercel account using your GitHub account.
-
After you log in, go to the project creation page and select
Continue with GitHuband give Vercel the permissions it asks for. -
Find the repository you just created and click
Import. For theFramework Preset, chooseOther. In theEnvironment Variablessection, add an entry whereNAMEisMONGO_SRVandVALUEis your MongoDB secret. -
Click
Deployand you will get a link likehttps://fritter-starter-abcd.vercel.app/where you can access your site.
Vercel will automatically deploy the latest version of your code whenever a push is made to the main branch.
The data that Fritter stores is divided into modular collections called "resources". The starter code has only two resources, "freets" and "users". The codebase has the following:
- A model file for each resource (e.g.
freet/model.ts). This defines the resource's datatype, which defines the resource's backend type, and should be a distilled form of the information this resource holds (as in ADTs). This also defines its schema, which tells MongoDB how to store our resource, and should match with the datatype. - A collection file for each resource (e.g.
freet/collection.ts). This defines operations Fritter might want to perform on the resource. Each operation works on the entire database table (represented by e.g.FreetModel), so you would operate on one Freet by usingFreetModel.findOne(). - Routes file (e.g.
freet/router.ts). This contains the Fritter backend's REST API routes for freets resource, and interact with the resource collection. All the routes in the file are automatically prefixed by e.g./api/freets. - Middleware file (e.g
freet/middleware.ts). This contains methods that validate the state of the resource before performing logic for a given API route. For instanceisFreetExistsinfreet/middleware.tsensures that a freet with givenfreetIdexists in the database
To add a resource or edit functionality of an existing resource:
- Create/modify files in the four above categories, making sure you have one model file, one collection one router file, and one middleware file per resource.
- It helps to go in the order that they're listed above, starting with the resource's datatype.
- In
freet/utils.tsanduser/utils.tsthere are type definitions for frontend representations of resources, and functions to convert from a backend resource type. Create/modify these as necessary.- An example: the frontend type definition for User lacks a
passwordproperty, because the frontend should never be receiving users' passwords.
- An example: the frontend type definition for User lacks a
For this assignment, we provide an extremely basic frontend for users to interact with the backend. Each box (html form) represents exactly one API route, with a textbox for each parameter that the route takes.
To add a new route to the frontend, two components need to be added: a form in public/index.html and a corresponding event handler in public/scripts/index.js. The form will allow the user to input any necessary fields for the route, and the event handler will take the values of these fields and make an API call to your backend.
For example, the form for the user creation route looks like:
<form id="create-user">
<h3>Create User</h3>
<div>
<label for="username">Username:</label>
<input id="username" name="username">
</div>
<div>
<label for="password">Password:</label>
<input id="password" name="password">
</div>
<input type="submit" value="Create User">
</form>
In public/scripts/user.js, there is an event handler for this form (event handlers are separated in files by concept, currently user.js and freet.js), which makes a POST request to the backend:
function createUser(fields) {
fetch('/api/users', {method: 'POST', body: JSON.stringify(fields), headers: {'Content-Type': 'application/json'}})
.then(showResponse)
.catch(showResponse);
}
Here, fields is a JSON object which contains key/value pairs, where the key is the name associated with the input field in the form and the value is whatever is entered on the frontend. For instance, in the form above, the first input field has name, username, which will be a key in fields object whose value is whatever has been entered as the username on the frontend. Thus, whatever name you attach to any input field is the same name you will can use to access the value entered in that input field from fields object.
To link the form and event handler together, there is an entry in the formsAndHandlers object (the key is the id attribute of the <form> tag and the value is the event handler function) in public/scripts/index.js:
const formsAndHandlers = {
'create-user': createUser,
...
};
MongoDB is how you will be storing the data of your application. It is essentially a document database that stores data as JSON-like objects that have dynamic schema. You can see the current starter code schema in freet/model.ts and user/model.ts.
You will be using Mongoose, a Node.js-Object Data Modeling (ORM) library for MongoDB. This is a NoSQL database, so you aren't constrained to a rigid data model, meaning you can add/remove fields as needed. The application connects to the MongoDB database using Mongoose in index.ts, where you see mongoose.connects(...). After it connects, you will be able to make mongoose queries such as FindOne or deleteMany.
In this starter code, we have provided user and freet collections. Each collection has defined schemas in */model.ts. Once you defined a Schema, you must create a Model object out of the schema. The instances of your model are what we call "documents", which is what is stored in collections. Each schema maps to a MongoDB collection and defines the shape and structure of documents in that collection, such as what fields the document has. When a schema is defined, documents in the collection must follow the schema structure. You can read more about documents here.
To create a new Schema, you first need to define an interface which represents the type definition. You can then create a new Schema object by declaring const ExampleSchema = new Schema<Example>(...) where Example is the type definition on the backend. Then, you can create a model like const ExampleModel = model<Example>("Example", ExampleSchema). You can see a more detailed schema in the model.ts files mentioned above.
Mongoose allows you to use schema validation if you want to ensure that certain fields exist. For example, if you look at freet/model.ts, you will find fields like
content: {
type: String,
required: true
}
within the schema. This tells us that the content field must have type String, and that it is required for documents in that collection. A freet must have a String type value for the content field to be added to the freets collection.
The following api routes have already been implemented for you (Make sure to document all the routes that you have added.):
This renders the index.html file that will be used to interact with the backend
Body
usernamesstring[] - the usernames (comma-separated) of the members of the circlenamestring - the name of the circle
Returns
- The newly created circle
- A success message
Throws
403if the user is not logged in409if a circle with the given name already exists400if any username passed in is not valid or if the circle name is empty413if the circle name is too long (> 50 chars)
Returns
- The request circle object
Throws
403if the user is not logged or the user isn't the owner of the circle404if the circle doesn't exist
Returns
- An array of the user's circles
Throws
403if the user is not logged in
Body (no need to add fields that are not being changed)
membersstring[] - the usernames (comma-separated) of the members of the circlenamestring - the name of the circle
Returns
- The updated circle
- A success message
Throws
403if the user is not logged in or if the user is not the owner of the circle404if the circle doesn't exist400if any username passed in as a circle member is not valid or if the circle name is empty413if the circle name is too long (> 50 chars)409if a circle with the updated name already exists
Returns
- A success message
Throws
403if the user is not logged in or if the user is not the owner of the circle404if a circle with the given id doesn't exist400if the user is trying to delete their auto-generated mutuals circle
Returns
- An array of the freets
- A success message
Returns
- An array of freets (includes freets posted to circles that the user is part of and all non-private freets)
Throws
403if the user is not logged in
GET /api/freets?authorId=id - Get all the freets by a specifc author that are visible to the current user
Returns
- If the user is looking for another user's freets: an array of freets (includes freets posted to circles that the user is part of and all non-private freets. doesn't include anonymous freets because that would expose the user who posted them)
- If the user is looking for their own freets: an array of all of their freets
Throws
403if the user is not logged in404if the author id provided doesn't exist
Returns
- An array of freets (all the freets posted by the users that the current user is following, sorted in descending order by date modified)
Throws
403if the user is not logged in
Returns
- The freet object
Throws
403if the user is not logged in or if the user is not a valid viewer for this freet404if the freet doesn't exist
Body
contentstring - the freet's contentanonymousboolean - whether the freet is anonymouscircleIdstring - the circle to post the freet to, if any
Returns
- The newly created freet object
Throws
403if the user is not logged in400if the freet content is empty or a stream of empty spaces413if the freet content is more than 140 characters
Body
contentstring - the freet's content
Returns
- The modified freet object
Throws
403if the user is not logged in or if the user is not the author of the freet404if the freet id is not valid400if the freet content is empty or a stream of empty spaces413if the freet content is more than 140 characters
Returns
- a success message
Throws
403if the user is not logged in or if the user is not the author of the freet404if the freet id is not valid
Returns
- an array of the freet replies
Throws
403if the user is not logged in or if the user can't access the freet404if the freet id is not valid
Body
contentstring - the reply's contentanonymousboolean - whether the reply is anonymous
Returns
- The newly created reply
Throws
403if the user is not logged in or if the user doesn't have access to the freet404if the freet doesn't exist400if the reply content is empty or a stream of empty spaces413if the reply content is more than 140 characters
Body
- None
Returns
- The newly created like
Throws
403if the user is not logged in or if the user doesn't have access to the freet404if the freet doesn't exist409if the user has already liked the freet
Body
- None
Returns
- A success message
Throws
403if the user is not logged in or if the user doesn't have access to the freet404if the freet doesn't exist or if the user hasn't already liked the freet
Returns
- An array of the freet likes
Throws
403if the user is not logged in or if the user doesn't have access to the freet404if the freet doesn't exist
Returns
- An array of the freet reports
Throws
403if the user is not logged in or if the user doesn't have access to the freet404if the freet doesn't exist
Body
- None
Returns
- The newly created report
Throws
403if the user is not logged in or if the user doesn't have access to the freet404if the freet doesn't exist409if the user has already reported the freet
GET /api/replies?authorId=id - Get all the replies by a specifc author that are visible to the current user
Returns
- If the user is looking for another user's replies: an array of replies (includes replies posted to circles that the user is part of and all non-private replies. doesn't include anonymous replies because that would expose the user who posted them)
- If the user is looking for their own replies: an array of all of their replies
Throws
403if the user is not logged in404if the author id provided doesn't exist
Returns
- an array of the replies
Throws
403if the user is not logged in or if the user is not the a valid viewer for this reply404if the reply id is not valid
Returns
- The reply object
Throws
403if the user is not logged in or if the user is not a valid viewer for this reply404if the reply doesn't exist
Body
contentstring - the reply's contentanonymousboolean - whether the reply is anonymous
Returns
- The newly created reply
Throws
403if the user is not logged in or if the user doesn't have access to the parent reply404if the reply doesn't exist400if the reply content is empty or a stream of empty spaces413if the reply content is more than 140 characters
Body
contentstring - the reply's content
Returns
- The updated reply
Throws
403if the user is not logged in or if the user is not the author of the reply404if the reply doesn't exist400if the reply content is empty or a stream of empty spaces413if the reply content is more than 140 characters
Returns
- A success message
Throws
403if the user is not logged in or if the user is not the author of the reply404if the reply doesn't exist
Body
- None
Returns
- The newly created like
Throws
403if the user is not logged in or if the user doesn't have access to the reply404if the reply doesn't exist409if the user has already liked the reply
Body
- None
Returns
- A success message
Throws
403if the user is not logged in or if the user doesn't have access to the reply404if the reply doesn't exist or if the user hasn't already liked the reply
Returns
- An array of the reply likes
Throws
403if the user is not logged in or if the user doesn't have access to the reply404if the reply doesn't exist
Returns
- An array of the reply reports
Throws
403if the user is not logged in or if the user doesn't have access to the reply404if the reply doesn't exist
Body
- None
Returns
- The newly created report
Throws
403if the user is not logged in or if the user doesn't have access to the reply404if the reply doesn't exist409if the user has already reported the reply
Body
username{string} - The user's usernamepassword{string} - The user's password
Returns
- A success message
- An object with user's details (without password)
Throws
403if the user is already logged in400if username or password is not in correct format format or missing in the req401if the user login credentials are invalid
Returns
- A success message
Throws
403if user is not logged in
Body
username{string} - The user's usernamepassword{string} - The user's password
Returns
- A success message
- An object with the created user's details (without password)
Throws
403if there is a user already logged in400if username or password is in the wrong format409if username is already in use
Body (no need to add fields that are not being changed)
username{string} - The user's usernamepassword{string} - The user's password
Returns
- A success message
- An object with the update user details (without password)
Throws
403if the user is not logged in400if username or password is in the wrong format409if the username is already in use
Returns
- A success message
- Also deletes all the items the user has created (freets, follows, replies, circles, etc.)
Throws
403if the user is not logged in
Body
- None
Returns
- The newly created follow
Throws
403if the user is not logged in404if the username to follow doesn't exist409if the follow already exists400if the user is trying to follow themselves
Returns
- A success message
Throws
403if the user is not logged in404if the username to unfollow doesn't exist or if the user isn't already following the username
Returns
- A array of the follows
Throws
403if the user is not logged in404if the username doesn't exist
Returns
- A array of the follows
Throws
403if the user is not logged in404if the username doesn't exist