Homework 6: Blog Part 1

Due March 19th at 11:59 PM

Topics: Backend, Express.js, Account Management Systems, JWT Authentication, Redis Cloud Database

In this homework, you will build the backend for a simple blogging platform using Express.js. This will be a multi-part homework that will continue after Spring Break with connecting to the frontend and deploying the application, but in this first part you will focus on building the backend and implementing the core functionality of the application. You will implement a RESTful API with Express.js that allows users to create accounts, log in, create blog posts, and view blog posts. You will also implement JWT authentication to secure the API endpoints and ensure that only authenticated users can access certain functionality. Finally, you'll connect to a Redis Cloud database to store user information, blog posts, and comments. This homework will give you practice working with backend development, Express.js, and authentication, which are all important skills for web developers.

Assignment Goals

  • Build the backend for a simple blogging platform
  • Learn to work with Express.js and RESTful API design
  • Implement JWT authentication for secure API endpoints
  • Get comfortable working with backend development and Redis Cloud databases

Introduction & Installation

Starter Code

Overview

For this homework, you will be building a blog using Express.js with protected routes that require JWT authentication. Your backend will connect to a Redis database to store user information and blog posts, and will provide a RESTful API for the future frontend to interact with. The core functionality of the application will include:

  • User account creation and management
  • User login and authentication with JWT
  • Creating, editing, and deleting blog posts
  • Creating and managing comments on multiple accounts

Files

The starter code for this homework contains the files for a server architecture using Express. This includes routes, controllers, services, and a Redis client. You should take some time to understand the provided file structure and purpose of each file as you will be building on top of this code to implement the functionality of the application.

Upon installing the starter code for this project, you should have the following files:

  • A config folder with redis.ts, a filled-out Redis Client
  • A controllers folder with various controllers for auth, comments, posts, and profiles
  • A routes folder with various routes for auth, posts, and profiles
  • A services folder with various services for auth, comments, posts, and profiles
  • A types folder for a helper type extension for Express Requests, and helpful types
  • A utils folder with auth.ts for mananging various authentication helper functions and middleware
  • index.ts, where your server will be started

Throughout this homework you can use the provided files to build your backend. Feel free to add various helper functions, middleware, and files as necessary to implement the functionality of the application, but make sure to keep your file structure organized and easy to navigate.

Dependencies

This homework will use various dependencies to help manage your backend server. Here is an overview of the main dependencies and libraries you will be using:

  • Express, a web framework for Node.js to build server-side applications
  • TypeScript and tsx, a TypeScript execution environment for Node.js
  • nodemon, a tool that helps develop Node.js applications by automatically restarting the server when file changes are detected
  • jsonwebtoken, a library for creating and verifying JSON Web Tokens (JWTs) for authentication
  • bcrypt, a library for hashing passwords
  • dotenv, a library for loading environment variables from a .env file
  • redis, a library for interacting with a Redis database
  • @paralleldrive/cuid2, a library for generating unique IDs

You should take some time to familiarize yourself with these dependencies and how they work. This homework specification will mention when to use these dependencies. Feel free to ask on Ed or in office hours if you are unsure about how to make use of any of these dependencies.

We've also provided a start script for development, with the command npm start. This runs both nodemon and tsx together to automatically restart the server when file changes are detected and to run the server with TypeScript.

Postman

Since this homework requires you to test your backend API endpoints, we recommend using Postman, a popular API testing tool that allows you to easily send requests to your API and view the responses. You can download Postman for free from their official website.

Postman will allow you to make API calls, like GET http://localhost:3000/register without the need to format a curl command. It also lets you easily add parameters, headers, and body data to your requests, and also provides multiple environments to manage different requests. Take advantage of Postman to thoroughly test your API endpoints with good and bad data, as we will also do the same when grading your homework!

TypeScript

For TypeScript use within this homework, you will need to make use of the Request and Response types from the Express library. However, these types don't include specific fields you have in the parameters or bodies of the API routes. You will need to add generics to the Request type to specify the types of the request parameters, query, and body for each route. For example, you may specify the ID of a URL parameter like so:

import type { Request, Response } from "express";

type Params = { id: string };

export function getUser(req: Request<Params>, res: Response) {
  const { id } = req.params; // this is now typed as a string
}

We've additionally provided you an type extension on the Request type to allow it to have an optional "user" field attached to it, so that you can work with the decoded JWT token payload a lot easier.

Instructions

Part 0: Set up your Redis Cloud DB and Environment Variables

Files: index.ts

We will be using Redis Cloud for this homework to host our Redis database. You should set up an account (with a Google email or GitHub) and create a Redis database instance to use for this homework. You should use the free tier, which provides more than enough resources for this homework. Once you have your Redis database set up, you should have the following information that you will need to connect to your database from your Express server:

  • A public endpoint (i.e. someting.cloud.redislabs.com)
  • A password (find this within the Security tab on your database page)

Next, you should set up your environment variables to securely store this information and use it within your Express server. You should create a .env file in the root of your project. A .env.example file has been provided in the starter code with the necessary variable names, so you can just copy that file to .env and fill in your own information. You should have the following environment variables set up:

  • JWT_TOKEN_SECRET (choose anything you want, you can use a generator to help)
  • REDIS_URL (formatted as redis://default:[PASSWORD]@[URL] with the information from your Redis Cloud dashboard)

Once you are all set up with Redis (and have made sure to run npm i to install dependencies), start setting up your Express application in index.ts. Make sure your Express server can boot up with npm start without any errors, and test a basic route just to make sure.

We have provided a pre-written Redis client that connects to your provided Redis URL. This client is primarily used in your services to interact with the Redis database. For now, you should test that you can connect to your Redis database from your Express server by importing the cilent, starting the server, then testing if you see 'Redis connected successfully' in your terminal. If you don't see this message, make sure to check your environment variables and Redis Cloud setup to ensure everything is correct.

Using environment variables

You'll notice in the Redis client provided that the dotenv package is used to load environment variables from your .env file, with the line dotenv.config(). This allows you to access your environment variables using process.env.VARIABLE_NAME anywhere in your code. For example, in the Redis client, the Redis URL is accessed with process.env.REDIS_URL.


Part 1: Authorization and Authentication with JWT

Files: auth.ts, authRoutes.ts, authController.ts, authService.ts

Now we can start implementing the core functionality of our application, starting with user account management and authentication. In this part, you will implement the backend functionality to allow users to create accounts, log in, and authenticate with JWTs.

You will be working in the auth routes, controllers, and services to implement the necessary API endpoints and logic for user registration, login, and JWT authentication. You should implement the following routes:

  • Method: POST /register
    • Description: Create a new user account. Validates that user input meets the required criteria and that the username is unique within the database. Hashes the user inputted password before creating a user with the role user.
    • Request Body (JSON):
      • username: string (required, 3-20 alphanumeric chars, no spaces)
      • password: string (required, min 8 chars, at least one letter and one number)
      • email: string (required, valid email format, like example@domain.com)
    • Response:
      • 201 Success: { message: 'Account created' }
      • 400 Error: { error: 'Missing fields: ...' } or validation error messages
      • 409 Error: { error: 'Username or email already exists.' }
    • Database Interaction:
      • Creates a Redis hash under the key user:[username] that stores the following fields:
        • username: the user's username
        • email: the user's email address
        • passwordHash: the user's password hashed securely with bcrypt
        • role: defaults to user
  • Method: POST /login
    • Description: Log in to an existing user account. Validates that the username and password are provided, checks that the username exists in the database, and compares the provided password with the stored hashed password using bcrypt. If the credentials are valid, generates a JWT token that includes the user's username and role, and returns it in the response.
    • Request Body (JSON):
      • username: string (required)
      • password: string (required)
    • Response:
      • 200 Success: { message: 'Login successful', jwt_token }
      • 400 Error: { error: 'Missing username or password' }
      • 401 Error: { error: 'Invalid credentials' }
    • Database Interaction:
      • Get a user by username from database to compare login credentials

For each of the routes above, and in the rest of this homework, you should start by defining the route in the routes file, that leads to a route handler in the controller file, and then calls any necessary functions in the service file to interact with the Redis database. Try each route one-by-one to make sure everything is working as you build it.

Additionally, in the login route, you will need to generate and sign a JWT token using the jsonwebtoken library. The token should include the user's username and role in the payload, and should be signed with the secret from your environment variables. You should return this token in the response when the user successfully logs in, as it will be used to authenticate future requests to protected routes. You can use auth.ts to implement to helper function.

Redis Data Storage

  • How Redis Stores Data: Redis is a key-value database, meaning it stores data as pairs of keys and values. Each key is a unique string, and its value can be a string, list, set, hash, or other data type. For user accounts, you should use a key format like user:[username] to uniquely identify each user. This makes it easy to look up, update, or delete a user's information by their username.
  • In the case of POST /register, we recommend you use a Redis hash to store the various fields of a user's account (username, email, passwordHash, role). A Redis hash allows you to store multiple field-value pairs under a single key, which is perfect for storing related information about a user. You can use the Redis commands hSet to set fields in the hash, and hGetAll to retrieve all the fields and values for a user when they log in.
  • Useful Redis Methods:
    • hSet(key, field, value): Set a field in a hash.
    • hGet(key, field): Get the value of a field in a hash.
    • hGetAll(key): Get all fields and values in a hash.
    • hmSet/Get(key, obj): Set/Get multiple fields in a hash.
    • hExists(key, field): Check if a field exists in a hash.
    • hDel(key, field): Delete a field from a hash.
    • rpush(key, value): Add a value to the end of a list.
    • lrange(key, start, stop): Get a range of elements from a list.
    • lrem(key, count, value): Remove elements from a list that match a value.

Part 2: Profile Routes

Files: profileRoutes.ts, profileController.ts, profileService.ts

After implementing the registration and login routes, you should now be able to see some data within your Redis database for the users you have created! As a sanity check, you can use a Redis GUI client like Redis Insight to easily view and manage your Redis database, which should available to you within the Redis Cloud dashboard. Check that your new users you've created are showin up in your database, with the hash structure you desire.

However, we don't want to just have user data sitting there in your database! Let's access and modify that data, with the following two routes for user profiles:

  • Method: GET /profile
    • Description: Fetch the profile information for the currently authenticated user.
    • Authentication: JWT required in Authorization header. username must be present in the token.
    • Response:
      • 200 Success: { username, email, role, description? }
      • 401 Error: { error: 'Unauthorized' }
      • 400 Error: { error: 'Missing username in token' }
      • 500 Error: { error: 'Failed to fetch profile' }
    • Database Interaction:
      • Fetches the user's profile from Redis, excluding the password field for security.
  • Method: PATCH /profile
    • Description: Update the profile information for the currently authenticated user. Partially updates the user's profile fields depending on what is provided in the request body.
    • Authentication: JWT required in Authorization header. username must be present in the token.
    • Request Body (JSON):
      • email: string (optional)
      • role: string (optional)
      • description: string (optional)
    • Response:
      • 200 Success: { username, email, role, description? }
      • 401 Error: { error: 'Unauthorized' }
      • 400 Error: { error: 'Missing username in token' }
      • 500 Error: { error: 'Failed to update profile' }
    • Database Interaction:
      • Updates the user's profile in Redis, excluding the password field for security.

For these routes and all future routes, you should implement a middleware function to authenticate the JWT token provided in the Authorization header of the request. For JWT tokens, the Authorization header should be formatted as Authorization: Bearer [token]. The middleware should verify the token using the secret from your environment variables, and if the token is valid, it should attach the decoded token payload (which includes the username and role) to the request object for use in the route handler. If the token is missing or invalid, the middleware should return a 401 Unauthorized error. You can implement this middleware function in auth.ts and use it in any routes that require authentication.


Part 3: Post Routes

Files: postRoutes.ts, postController.ts, postService.ts

Now we get to the meat of this app: you'll be implementing the routes to create, read, update, and delete (CRUD) blog posts! Implement the following routes:

  • Method: POST /posts
    • Description: Create a new blog post for the authenticated user. Each blog Post in the database should contain a unique ID created with @paralleldrive/cuid2, the username of the creator, a title, content, and a timestamp (Date) for when it was created.
    • Authentication: JWT required in Authorization header. username must be present in the token.
    • Request Body (JSON):
      • title: string (required)
      • content: string (required)
    • Response:
      • 201 Success: {post}: Post
      • 500 Error: { error: 'Failed to create post' }
    • Database Interaction:
      • Stores the post object in the database with a unique ID, username, title, content, and timestamp.
  • Method: GET /posts/:user
    • Description: Get all blog posts for the specified user.
    • Authentication: No authentication required.
    • Request Parameters:
      • user: string (required, the username of the user)
    • Response:
      • 200 Success: Array of Post objects
      • 500 Error: { error: 'Failed to fetch posts' }
    • Database Interaction:
      • Fetches all posts for the specified user from the database.
  • Method: GET /posts/:user/:id
    • Description: Get a specific blog post by its ID for the specified user.
    • Authentication: No authentication required.
    • Request Parameters:
      • user: string (required, the username of the user)
      • id: string (required, the unique ID of the post)
    • Response:
      • 200 Success: Post object
      • 404 Error: { error: 'Post not found' }
      • 500 Error: { error: 'Failed to fetch post' }
    • Database Interaction:
      • Fetches a specific post by its ID for the specified user from the database.
  • Method: PATCH /posts/:id
    • Description: Update a specific blog post by its ID for the authenticated user. Partially updates the post fields depending on what is provided in the request body.
    • Authentication: JWT required in Authorization header. username must be present in the token.
    • Request Parameters:
      • id: string (required, the unique ID of the post)
    • Request Body (JSON):
      • title: string (optional)
      • content: string (optional)
    • Response:
      • 200 Success: Updated post object
      • 404 Error: { error: 'Post not found' }
      • 500 Error: { error: 'Failed to update post' }
    • Database Interaction:
      • Partially updates the specified post in the database with the provided fields.
  • Method: DELETE /posts/:id
    • Description: Delete a specific blog post by its ID for the authenticated user.
    • Authentication: JWT required in Authorization header. username must be present in the token.
    • Request Parameters:
      • id: string (required, the unique ID of the post)
    • Response:
      • 200 Success: { message: 'Post deleted' }
      • 404 Error: { error: 'Post not found' }
      • 500 Error: { error: 'Failed to delete post' }
    • Database Interaction:
      • Deletes the specified post from the database.

For storing posts in Redis, you can choose the data structure that you think is best suited for this use case. One possible approach is to store each post as a hash with a unique key like post:[id], and then maintain a list of post IDs for each user under a key like user:[username]:posts. This way, you can easily fetch all posts for a user by getting the list of post IDs and then fetching each post by its ID. However, feel free to choose any data structure and organization that you think works best!

Additionally, be mindful that each created post must have a unique ID. You can use the @paralleldrive/cuid2 library to generate unique IDs for each post. Make sure to include this ID in the post object that you store in the database, and use it to identify posts for fetching, updating, and deleting.


Part 4: Comment Routes

Files: postRoutes.ts, commentController.ts, commentService.ts

Finally, we'll implement some routes to allow other users to comment on blog posts! This will allow you to practice implementing routes that interact with data from multiple users, and practice implementing pagination, like you've experienced before in HW5 with the Pokedex. You should implement the following routes within postRoutes.ts:

  • Method: POST /posts/:id/comments
    • Description: Create a new comment on the specified post. A Comment consists of a unique comment ID, the post ID it belongs to, the username of the commenter, the content of the comment, the post author, and a createdAt timestamp.
    • Authentication: JWT required in Authorization header. username must be present in the token.
    • Request Parameters:
      • id: string (required, the unique ID of the post)
    • Request Body (JSON):
      • content: string (required)
    • Response:
      • 201 Success: {comment}: Comment
      • 500 Error: { error: 'Failed to create comment' }
    • Database Interaction:
      • Stores the comment object in the database with a unique ID, post ID, username, content, post author, and timestamp.
  • Method: GET /posts/:id/comments
    • Description: Get all comments for the specified post. The response to this route should be paginated.
    • Authentication: No authentication required.
    • Query Parameters:
      • limit: number (optional, default: 5)
      • offset: number (optional, default: 0)
    • Request Parameters:
      • id: string (required, the unique ID of the post)
    • Response:
      • 200 Success: Array of Comment objects
      • 500 Error: { error: 'Failed to fetch comments' }
    • Database Interaction:
      • Fetches comments for the specified post from the database with pagination support.
  • Method: DELETE /posts/:id/comments/:commentId
    • Description: Allows an authenticated user to delete a specific comment on their own post. User and post author must match for deletion to be successful.
    • Authentication: JWT required in Authorization header. username must be present in the token.
    • Request Parameters:
      • id: string (required, the unique ID of the post)
      • commentId: string (required, the unique ID of the comment)
    • Response:
      • 200 Success: { message: 'Comment deleted' }
      • 404 Error: { error: 'Post/Comment not found or not authorized' }
      • 500 Error: { error: 'Failed to delete comment' }
    • Database Interaction:
      • Deletes the specified comment from the post if the user is authorized.

Your implementation of comments should allow a user to comment on their own post, and other users to comment on other users' posts. This authorship of comments should be reflected in the comment data stored in the database.

Submission

README

Answer the provided reflection questions within the starter code README file. In this reflection, you will also indicate whether or not you used AI, and also document your usage of AI as well. Please don't forget this step, as it is important feedback for the homework and the content of the course!

Submission

Submit your code through Gradescope as a .zip file that contains your project. Make sure your project includes all files you worked on during this homework and your README.md file. Make sure the submitted file structure within your submission is exactly or similar to the file structure you used to run and develop the project. Points will be taken off for malformed project structures in the final submission! Additionally, do not include the .env folder in your submission! We will be grading your project with our own database to test how your backend interacts with it, so there is no need to include your own environment variables in the submission.

Before you submit, make sure you lint your code for style errors using the command npm run lint. We will be using the eslint rules provided by Vite for linting this project.

For this homework, we've provided a rubric file named RUBRIC.md in the starter code. Make sure to read through it carefully and ensure that your submission meets all the requirements outlined in the rubric. This will help you maximize your score and ensure that you've covered all necessary aspects of the assignment.