CSE 341: Web Services

W01 Assignment: Part 3 - Implement the Plan

Overview

In this part of the assignment, you will implement each planned task in order. You will build a small Express API that reads book data from MongoDB.

Because this is Week 01, this activity will guide you through each major step. In future weeks, you will research and implement the tasks on your own.

No code reviews for Week 01

In future assignments, you will be working with your team and reviewing each person's pull requests before they are merged into the main branch. Because Week 01 is an individual assignment, you will not perform code reviews for this code. But you will still follow the same process of creating feature branches and pull requests.

Implementation strategy

As you go through this activity, you will implement one issue at a time. For each one, you should first understand the requirement, then implement it and make sure it is working. For Issues 1, 2, and 3, you will create a pull request and merge it back into the main branch. For Issue 4, you will verify the deployment and close the issue manually.

Pull Requests

A pull request is a way for you to show the changes you want so they can be reviewed and merged into the project. To create a pull request, you will do your work on a separate feature branch on your computer, then push that branch up to GitHub, and request that the branch is merged into main. When the feature branch has been merged into main at GitHub, you will pull the updated main branch down to your computer and delete the local feature branch.

Why use Pull Requests

Creating pull requests for the coding issues may seem like a lot of work when you could easily just commit and push to the main branch. But remember that pull requests allow for code review and discussion before changes are merged, which will be very important on your team project and in your career.

With this in mind, this assignment is designed to help you create and merge several pull requests, so that you are comfortable with them in a simple, individual setting before you use them with your team which is more complicated.

Activity Instructions

Implement each issue in the order listed below.


Issue 1: Create the books collection and connect the app to MongoDB

First, open the issue you created for this task at GitHub. Read through the issue and make sure you understand everything that you need to do.

In short, for this issue you will create the books collection, add the initial book documents, and follow the same MongoDB connection steps you already practiced in the NoSQL learning activity.

Start a new branch for this issue

When you are ready to begin, create a feature branch so this work can become its own pull request.

  1. Create and switch to a new branch for this issue.
  2. Use a branch name such as connect-to-mongodb.
  3. Create the new branch by running the following command in your VS Code Terminal window:
    git switch -c connect-to-mongodb

Why Use the Terminal for Git Commands?

For this course, you will use the terminal for Git commands so that you become very familiar with the most common Git commands and what each one does.

Once you have a solid understanding of these commands, graphical Git tools will feel much easier to use and will not seem like magic.

This level of Git knowledge is also something employers notice. It shows that you understand the process behind the buttons.

Create the books collection and seed data

Use MongoDB Atlas Data Explorer in your browser to create the collection that your routes will read from.

  1. Sign in to MongoDB Atlas and open the project that contains the cluster you created in the learning activity.
  2. In the Atlas sidebar, under Database, open Data Explorer.
  3. In the Connections sidebar, select or hover over your cluster. If a database named cse341-books-db does not already exist, open the control to create a database.
  4. When the create database form appears, enter cse341-books-db as the database name and books as the collection name.
  5. Click Create Database.
  6. In the Connections sidebar, select the new cse341-books-db database, then select the books collection.
  7. Click Insert Document and switch to JSON mode if the field-by-field editor is displayed.
  8. Replace the sample content with a book document that includes the fields id, author, title, and publicationDate.
  9. Use a simple id such as b1 for the first document, then click Insert.
  10. Repeat this process to add at least two more documents using ids such as b2 and b3.
  11. Remember AI can be helpful

    Remember that AI can be really helpful in creating sample data like this. You can give it a prompt such as:

    Generate 3 sample book documents for MongoDB that have string fields for id, author, title, and publicationDate fields. The id should be of the format "b1", "b2", "b3".

  12. After the documents are inserted, verify that Data Explorer shows all of them in the books collection.

Why are we using our own id field?

Recall that for this assignment, you will use simple values such as b1, b2, and b3 for the custom id field in addition to the actual _id field that MongoDB creates automatically, so your route testing is easier.

Connect to the Database

In the learning activity, you created a database connection for your app. In this step you will verify that everything is working and that you can access your books collection.

For testing purposes, update your server.js file so that during startup, it connects to the database and logs the book documents.

  1. In your .env file add a MONGODB_DB_NAME variable for the database name such as:
    MONGODB_DB_NAME=cse341-books-db
  2. In your server.js file, verify that you have imported the connectToDb function and that it is called during startup.
  3. Make sure your database name is cse341-books-db for this assignment instead of practice.
  4. Add an import for the getDb function in your server.js file.
  5. In your startServer function, immediately after calling the connectToDb function, add a call get all the books and log them to the console using the following line:
    const books = await getDb().collection('books').find({}).toArray();
    console.log('Book documents:', books);
    
  6. Verify that the book documents are logged to the console.
    1. Start the app with npm run dev.
    2. Verify that the server starts without a database connection error.
    3. Visit http://127.0.0.1:3000 and make sure the root route still responds successfully.
    4. Verify that the data from the books collection is displayed to the console.
Sample Code (click to expand)
// server.js
import app from './app.js';
import { connectToDb, getDb } from './src/db/connect.js';

const PORT = process.env.PORT;

if (!PORT) {
  throw new Error('PORT is not defined. Make sure your local npm scripts reference the .env file with --env-file=.env, or define PORT in your hosted environment settings.');
}

const startServer = async () => {
  try {
    await connectToDb();
    const books = await getDb().collection('books').find({}).toArray();
    console.log('Book documents:', books);    

    app.listen(PORT, () => {
      console.log(`Server listening on port ${PORT}`);
    });
  } catch (error) {
    console.error('Database connection failed:', error.message);
    process.exit(1);
  }
};

await startServer();

Having trouble?

If you run into problems, return to the W01 Learning Activity: NoSQL Databases and MongoDB and ask AI for help.

Remove your temporary testing code

Once you have verified that your implementation works correctly, remove the testing code you just added.

  1. In server.js, remove the code that logs the books to the console.
  2. In server.js, remove the import statement for getDb.
  3. You should still have code that connects to the database, but you should not have any code that gets the books and logs them to the console.

Verify the issue Test Plan

Before you create a pull request, open the GitHub issue you created for this work. At the end of the issue, find the Test Plan and go through each item.

Test Plan:
- The `cse341-books-db` database exists.
- The `books` collection exists with at least 3 documents.
- Each book document includes `id`, `author`, `title`, and `publicationDate`.
- Local app can connect to MongoDB without connection errors.
- Connection values are stored using environment variables (no secrets committed).
  1. Confirm that the cse341-books-db database exists.
  2. Confirm that the books collection exists with at least 3 documents.
  3. Confirm that each book document includes id, author, title, and publicationDate.
  4. Confirm that the local app can connect to MongoDB without connection errors.
  5. Confirm that connection values are stored using environment variables and that no secrets are committed.

Create and merge a pull request

To finish this step, create a pull request for your collection setup and database connection work.

  1. Commit your changes with a clear message:
    git add .
    git commit -m "Connected app to MongoDB"
  2. Push the branch to GitHub:
    git push origin connect-to-mongodb
  3. Create a pull request from your branch to main.
    1. Open your GitHub repository in a browser.
    2. If GitHub shows a prompt for your recently pushed branch, click Compare & pull request.
    3. If you do not see that prompt, click the Pull requests tab, then click New pull request.
    4. Make sure the base branch is main and the compare branch is your feature branch.
    5. Add a clear pull request title that matches the work you completed.
  4. For the pull request description make sure to include the following:
  5. Review the changes, merge the pull request, and confirm the issue is closed.
  6. Follow the instructions to delete the feature branch on GitHub.

Automatically link and resolve issues

When you include #1 in your description, GitHub will link your pull request to your issue.

When you use the keywords Resolves, Closes, or Fixes, GitHub will automatically close the issue when you merge the pull request. If you do not use one of those keywords, you will need to close the issue manually.

Clean up your branches on your local computer

After merging your pull request, you can delete the feature branch from your computer. In your VS Code terminal, do the following:

  1. Switch back to the main branch:
    git switch main
  2. Pull the latest changes:
    git pull origin main
  3. Delete the local feature branch:
    git branch -d connect-to-mongodb

Issue 2: Implement a route to retrieve all books

First, open the issue you created for this task at GitHub. Read through the issue and make sure you understand everything that you need to do.

In short, for this issue you will implement GET /books to return all books from the database. When you are finished, the endpoint returns a JSON array and status 200.

MVC Overview

In your previous course, you used the MVC pattern to help organize code. MVC stands for Model, View, and Controller.

In this API project, you will still use the same basic pattern, but instead of returning rendered views, your controllers will return JSON data.

For this issue, your model will retrieve data from MongoDB, your controller will decide how to respond, and your router will connect the URL to the controller function.

If you need a deeper refresher, refer to Week 03 of the CSE 340 course materials.

Create a new Git branch for this issue

In your terminal, create a new branch for this issue:

git switch -c add-get-books-route

Create the model function in src/models/books.js

The first step is to create the model. The model's job is to talk to the database and return data.

  1. Create a new folder named src/models if it does not already exist.
  2. Create a file named src/models/books.js.
  3. In the model file, import getDb from ../db/connect.js.
  4. Create an async function named getAllBooks. (Remember to use arrow notation according to the course coding standards.)
  5. The function should do the following:
    • Get the database object by calling getDb().
    • Access the books collection.
    • Call find({}) to get all documents from the collection.
    • Call toArray() to convert the results into a JavaScript array.
    • Return that array.
  6. Export the model function so it can be used in the controller.
Sample Code (click to expand)
// src/models/books.js
import { getDb } from '../db/connect.js';

const getAllBooks = async () => {
  const db = getDb();
  const collection = db.collection('books');
  const books = await collection.find({}).toArray();
  return books;
};

export { getAllBooks };

Querying MongoDB

When you call find({}), MongoDB does not immediately give you back a normal array. It gives you a cursor, which is an object you can use to work through the results.

Calling toArray() tells MongoDB to gather those results into a JavaScript array so you can return them more easily in your API response.

Create the controller function in src/controllers/books.js

The controller receives the request, calls the model, and decides what response to send back.

  1. Create a new folder named src/controllers if it does not already exist.
  2. Create a file named src/controllers/books.js.
  3. In the controller file, import getAllBooks from ../models/books.js.
  4. Create an async controller function named getBooksHandler that accepts req and res.
  5. Inside that function:
    • wrap your logic in a try/catch block
    • call getAllBooks() and store the returned array in a variable
    • send the successful response with return res.status(200).json(books)
    • if something goes wrong, log a short message on the server and return a safe JSON response with status 500
  6. Export the controller function.
Sample Code (click to expand)
// src/controllers/books.js
import { getAllBooks } from '../models/books.js';

const getBooksHandler = async (req, res) => {
  try {
    const books = await getAllBooks();
    return res.status(200).json(books);
  } catch (error) {
    console.error('GET /books failed:', error.message);
    return res.status(500).json({ message: 'Internal server error' });
  }
};

export { getBooksHandler };

What does res.status(200).json(books) do?

The status(200) part sets the HTTP status code to 200, which tells the client that the request succeeded.

The json(books) part sends the JavaScript array back to the client as JSON and sets the correct response headers automatically.

Together, this is a common Express API pattern: set the status code, then send JSON data.

Create the router in src/router.js

The router's job is to connect URL patterns to controller functions.

  1. Create a file named src/router.js.
  2. Import Express.
  3. Create a router with express.Router().
  4. Import getBooksHandler from ./controllers/books.js.
  5. In the router file:
    • add a route for GET /books
    • use getBooksHandler as the controller function for that route
  6. Export the router as the default export.
Sample Code (click to expand)
// src/router.js
import express from 'express';
import { getBooksHandler } from './controllers/books.js';

const router = express.Router();

router.get('/books', getBooksHandler);

export default router;

Update app.js to use the router

Now connect your new router to the main Express app.

  1. Open app.js.
  2. Remove any direct /books route code if you started writing it there.
  3. Remove the practice route for /trails from the learning activity if it is there.
  4. Remove the import statement for getDb from the learning activity if it is there. This function should only be used in the model, not in the app code here.
  5. Import the router from ./src/router.js.
  6. In app.js:
    • keep app.use(express.json())
    • add app.use(router) so the main app uses the new router
Sample Code (click to expand)
// app.js
import express from 'express';
import router from './src/router.js';

const app = express();

app.use(express.json());
app.use(router);

app.get('/', (req, res) => {
    return res.status(200).json({ message: 'Server is running' });
});

export default app;

Run and verify the route

Before you create a pull request, open the GitHub issue you created for this work. At the end of the issue, find the Test Plan and use it as your checklist.

Test Plan:
- `GET /books` returns an array in JSON.
- Response status is 200 on success.
- Each returned book includes `id`, `author`, `title`, and `publicationDate`.
- Response uses `application/json`.
- Unexpected failures return `{ "message": "Internal server error" }` with status 500.
  1. Start the app with npm run dev.
  2. Open http://127.0.0.1:3000/books in your browser or test it with curl.
  3. Verify that GET /books returns an array in JSON.
  4. Verify that the response status is 200 on success.
  5. Verify that each returned book includes id, author, title, and publicationDate.
  6. Verify that the response uses application/json.
  7. Confirm that unexpected failures return { "message": "Internal server error" } with status 500. If you cannot easily cause a server error, review your controller code to make sure the catch block returns this response.
  8. If the route does not work, trace the flow in order: router to controller to model to MongoDB.

Testing API routes

To test these simple API routes, you can visit the URL in a browser or use a tool like curl that is a command line tool to send a web request in the terminal.

In the future, when you are building more complex applications, you will use more advanced tools for testing.

Sample Code (click to expand)

You can run the following command in your terminal to test your route:

curl http://127.0.0.1:3000/books

Create and merge a pull request

To finish this step, create a pull request for the new /books route.

  1. In your terminal, commit your changes with a clear message:
    git add .
    git commit -m "Added GET /books route"
  2. Push the branch to GitHub:
    git push origin add-get-books-route
  3. Return to GitHub and create a pull request for this branch.
    • Make sure you are merging from add-get-books-route to main
    • Verify the issue number associated with this issue (for example, #2)
    • In your pull request description, include Resolves #2, Closes #2, or Fixes #2 (assuming #2 is the GitHub issue number) so the issue is closed automatically when the pull request is merged.
    • In the pull request description, include the steps you have taken to verify it. You can copy and paste the steps of the test plan from the issue if you have verified them.
  4. Merge the pull request after verifying the changes.
  5. After merging, you can safely delete the feature branch at GitHub.

Clean up your branches on your local computer

After merging your pull request, you can delete the feature branch from your computer. In your VS Code terminal, do the following:

  1. Switch back to the main branch:
    git switch main
  2. Pull the latest changes:
    git pull origin main
  3. Delete the local feature branch:
    git branch -d add-get-books-route

Issue 3: Implement a route to retrieve one book by id

First, open the issue you created for this task at GitHub. Read through the issue and make sure you understand everything that you need to do.

In short, for this issue you will implement GET /books/:id to return one book by its id. When you are finished, an id for an existing book should return a JSON object with status 200 and a missing id should return a 404 JSON message.

Follow the same MVC pattern

For this issue, you will follow the same pattern you used for GET /books: update the model, update the controller, update the router, and then verify the route.

This keeps your code organized and makes it easier to add more routes later.

Create a branch for this issue

Before you start coding, create and switch to a new branch for this issue.

  1. In your terminal, create a new branch for this issue:
    git switch -c get-book-by-id

Add a model function for retrieving one book

Start in the model again. The model should retrieve one specific book document whose id matches the value passed to the function.

  1. Open src/models/books.js.
  2. Create a new async function named getBookById.
  3. Give the function one parameter, such as bookId.
  4. Inside that function:
    • use getDb() to get the database instance
    • select the books collection
    • call findOne({ id: bookId }) to request one document whose id field matches the route parameter
    • return the matching book document
  5. Export the new model function.

Why use findOne() here?

Your GET /books route used find() because it needed many documents. This route needs only one document, so findOne() is the better choice.

The filter object { id: bookId } tells MongoDB to look for a single document whose custom id field matches the route value.

Sample Code (click to expand)
const getBookById = async (bookId) => {
  const db = getDb();
  const collection = db.collection('books');
  const book = await collection.findOne({ id: bookId });
  return book;
};

Add a controller function for GET /books/:id

The controller should read the route parameter, call the model, and decide whether to return a 200, 404, or 500 response.

  1. Open src/controllers/books.js.
  2. Import your new model function: import { getAllBooks, getBookById } from '../models/books.js';
  3. Create a new async controller function named getBookByIdHandler that accepts req and res.
  4. Inside that function:
    • read the route value from req.params.id
    • wrap your logic in a try/catch block
    • call getBookById(requestedId)
    • if no book is returned, respond with status 404 and { "message": "Book not found" }
    • if a book is returned, send the response with return res.status(200).json(book)
    • if something unexpected goes wrong, log a short message and return a safe 500 JSON response
  5. Export the controller function.
Sample Code (click to expand)
const getBookByIdHandler = async (req, res) => {
  const requestedId = req.params.id;

  try {
    const book = await getBookById(requestedId);

    if (!book) {
      return res.status(404).json({ message: 'Book not found' });
    }

    return res.status(200).json(book);
  } catch (error) {
    console.error('GET /books/:id failed:', error.message);
    return res.status(500).json({ message: 'Internal server error' });
  }
};

What is new in this controller?

This is the first route in the assignment that needs two successful outcomes to be distinguished: one when the book exists and one when it does not.

A missing record is not the same as a server failure. That is why this controller returns a 404 for "not found" and reserves 500 for unexpected server problems.

Update the router

Add the new route to the router so Express knows which controller to run when a client requests /books/:id.

  1. Open src/router.js.
  2. Import getBookByIdHandler from ./controllers/books.js.
  3. In the router file:
    • add a route for GET /books/:id
    • use getBookByIdHandler as the controller for that route
  4. Keep the existing GET /books route in place.
Sample Code (click to expand)
router.get('/books/:id', getBookByIdHandler);

Run and verify the route

Before you create a pull request, open the GitHub issue you created for this work. At the end of the issue, find the Test Plan and use it as your checklist.

Test Plan:
- An existing id returns one book object in JSON.
- The returned book includes `id`, `author`, `title`, and `publicationDate`.
- A missing id returns `{ "message": "Book not found" }` with status 404.
- Response status is 200 on success.
- Response uses `application/json`.
- Unexpected failures return `{ "message": "Internal server error" }` with status 500.
  1. Start the app with npm run dev.
  2. Test the route using an id from one of your seed records, such as b1.
  3. Verify that an existing id returns one book object in JSON.
  4. Verify that the returned book includes id, author, title, and publicationDate.
  5. Test the route again with an id that does not exist.
  6. Verify that a missing id returns { "message": "Book not found" } with status 404.
  7. Verify that the response status is 200 on success.
  8. Verify that the response uses application/json.
  9. Confirm that unexpected failures return { "message": "Internal server error" } with status 500. If you cannot easily cause a server error, review your controller code to make sure the catch block returns this response.
  10. If the route does not work, trace the flow in order: router to controller to model to MongoDB.
Issue 3 Sample Checks (click to expand)
# Example checks
curl -i http://localhost:3000/books/b1
curl -i http://localhost:3000/books/not-a-real-id

Create and merge a pull request

To finish this step, create a pull request for the new /books/:id route.

  1. Commit your changes with a clear message:
    git add .
    git commit -m "Add GET /books/:id route"
  2. Push the branch to GitHub:
    git push origin get-book-by-id
  3. Return to GitHub and create a pull request for this branch.
    • Make sure you are merging from get-book-by-id to main
    • Verify the issue number associated with this issue (for example, #3)
    • In your pull request description, include Resolves #3, Closes #3, or Fixes #3 (assuming #3 is the GitHub issue number) so the issue is closed automatically when the pull request is merged.
    • In the pull request description, include the steps you have taken to verify it. You can copy and paste the steps of the test plan from the issue if you have verified them.
  4. Merge the pull request after verifying the changes.
  5. Follow the instructions to delete the feature branch on GitHub.

Clean up your branches on your local computer

After merging your pull request, you can delete the feature branch from your computer. In your VS Code terminal, do the following:

  1. Switch back to the main branch:
    git switch main
  2. Pull the latest changes:
    git pull origin main
  3. Delete the local feature branch:
    git branch -d get-book-by-id

Issue 4: Deploy and test the application

First, open the issue you created for this task at GitHub. Read through the issue and make sure you understand everything that you need to do.

In short, for this issue you will check that the finished API deployed correctly, connects to the database, and has both routes working. Your app should have been deployed already when you merged your previous pull requests to main. This issue is where you confirm that everything worked correctly and fix any deployment problems that may have happened.

No Git Branch Needed

Because this issue is all about deployment and configuration, you will likely not need to make any local code changes. For that reason, you do not need to create a new branch or pull request for this issue. Instead, when you are finished with the issue, you will close it directly at GitHub.

Complete the following:

Add your environment variables to Render.com

When you run the app locally, it reads the connection string and database name from your .env file. When you deploy to Render, there is no .env file, so you need to add those values to Render's environment variable settings.

Render will make those variables available to your app as part of the process.env object, just like your local .env file does.

  1. Go to your Render.com dashboard and open your project.
  2. Click the Environment tab.
  3. Click the Add Environment Variable button.
  4. Add the following variables with the same values as your local .env file:
    • MONGODB_URI
    • MONGODB_DB_NAME

Do not add a PORT variable

Render automatically provides a PORT environment variable to your app, so you do not need to add that one yourself. In fact, if you add your own PORT variable, it might conflict with the one Render provides and cause problems.

Allow Render to connect to MongoDB Atlas

MongoDB Atlas restricts database connections so they only come from trusted places. When you set up Atlas earlier, you likely added an IP address that works for your own computer. Now that your app is hosted on Render, you also need to add the outbound IP address ranges that Render will use when your deployed app connects to Atlas.

  1. Log in to Render and open the dashboard for your deployed web service.
  2. Select Connect, then select Outbound IP Addresses.
  3. Copy the outbound IP address ranges Render shows for your service. Render may give you one or more ranges, such as:
    74.220.48.0/24
    74.220.56.0/24
  4. Open MongoDB Atlas in a browser and sign in.
  5. In the left navigation, under the Security section, select Database and Network Access.
  6. Under Network Access, open the IP Access List.
  7. Click + Add IP Address.
  8. Add the outbound IP address range you copied from Render.
  9. If Render gave you more than one outbound IP address range, add each range to the IP access list. You may need to add multiple lines or repeat the + Add IP Address step for each range.
  10. Click Confirm and wait for Atlas to finish updating the access list.

Verify the Render deployment

Render should have automatically built and deployed the latest version of your app after your previous pull requests were merged to main. Open the GitHub issue you created for this work. At the end of the issue, find the Test Plan and use it as your checklist.

Test Plan:
- Deployed `GET /books` returns an array with status 200.
- Deployed `GET /books/:id` returns a book for an existing id with status 200.
- Deployed `GET /books/:id` returns 404 for a missing id.
- Deployed not-found and server-error responses return safe JSON messages.
- The final deployed URL is recorded in the README or in an issue comment.
  1. Open your Render dashboard and select your web service.
  2. Open the deploy details or logs for the most recent deploy.
  3. If the last build failed, now that you have updated the MongoDB Atlas connection settings, you may need to start a manual deploy.
    • Click the Manual Deploy button, select From last commit, and wait for Render to build and deploy the app.
    • If there are any problems during the build or deployment, look at the Render logs to figure out what is happening, then address the issue shown in the logs.
  4. Wait until Render reports that the deployment succeeded.
  5. Open the Render-provided URL and verify that the root route responds successfully.
  6. Verify that at your Render URL, GET /books returns an array with status 200.
  7. Verify that at your Render URL, GET /books/:id returns a book for an existing id with status 200.
  8. Verify that at your Render URL, GET /books/:id returns 404 for a missing id.
  9. Verify that at your Render URL, not-found and server-error responses return safe JSON messages. If you cannot easily cause a server error, review your deployed code to make sure the catch blocks return safe JSON responses.
Issue 4 Sample Checks (click to expand)
# Replace with your actual deployed URL
curl -i https://<your-deployed-url>/books
curl -i https://<your-deployed-url>/books/b1
curl -i https://<your-deployed-url>/books/does-not-exist

Close the GitHub issue

You do not need to create a pull request for this final deployment and testing issue because you are not writing new code for this step.

  1. Return to the GitHub issue for this issue.
  2. Add a comment with the deployed URL and a short note about what you tested.
  3. Close the issue manually in GitHub.

Next Step

Complete the other parts of the Week 01 assignment.

After you have completed all parts of the assignment, return to Canvas to submit a quiz.

Other Links: