CS 312 Software Development

CS 312 - Practical Seven

Goals

  • Implement a simple API server using Next.js
  • Learn how to test a server API

Prerequisites

  1. Visit the Practical 7 page on Replit (or click the 'Start Project' link for Practical 7 on our team page).

  2. Click through to the GitHub classroom assignment to create your private repository.

  3. Return to the assignment on Replit and go to the Version Control panel.

  4. Click the button to create a new git repository (not the one for connecting to an existing repo).

  5. Open the shell and follow the instructions on your GitHub repository for connecting an existing repository:

    1. git remote add origin repo-name where repo-name is the name of your repository (e.g., https://github.com/csci312-s21/practical07-ChristopherPAndrews.git)
    2. git branch -m main
    3. git push -u origin main

Overview

We are going to create a series of routes that implement the Film Explorer API. We will be using the API routes facility provided by Next.js to implement them as micro-services rather than writing a full server. We will then briefly touch upon how we can test these routes.

Data persistence

One of the most significant reason to introduce a server to our applications is to provide data persistence. Typically this will mean storing information in a database. However, since we have not yet covered databases, our server is going to use the file system as data storage.

As before, we have a JSON file containing our data (data/films.json). However, rather than including it with the client code on the front-end, we will be accessing it on demand from the API routes. Also, because our interactions will alter the data (as we rate films), I have added a second file (data/films-orig.json) which will serve as our pristine copy so we can revert changes.

I have provided the functions you need to interact with our data store. You will find them in lib/backend-utils.js. The three functions are:

  • readData() - this returns an Array of our film objects
  • saveData(films) - this takes in an Array of film objects (films) and writes it out to our storage
  • resetData() - this is a convenience function that resets our data store to our pristine copy (use cautiously, it does no error checking and will overwrite any changes in films.json)

Take a moment to read these functions over. I particularly want to call your attention to our use of fs and path. These are libraries available in Node.js for working with the filesystem. These are only available through Node.js and not in the browser. This marks these functions as being server specific. Next.js will complain if you try to import these libraries (or backend-utils.js) into any front-end code.

Get all the films

The first route we will implement will be /api/films, which should return all films.

Recall that we create our API Routes in Next.js by creating files in the the api directory. So, inside of the pages directory, create an api directory.

Create a films directory inside of that.

Create an index.js inside of that.

Type in the following (yes, type, don't paste):

import nc from 'next-connect';

const handler = nc().get((req, res) => {});

export default handler;

This is the general template we will follow for all of our endpoints. We create a handler with next-connect, and then we create endpoints with HTTP verbs.

Returning all of the films is pretty straightforward.

First, we need to import readData so we can access the data. Add this at the top with the other import statement:

import { readData } from "../../../lib/backend-utils";

You can now implement the endpoint. Inside of the function, add the following:

const films = readData();
res.status(200).json(films);

That's it -- you just wrote your first endpoint. That second line does all of the heavy lifting, it sets the status code of the response (200 -- everything is ok) and loads the films into the body as a JSON string.

Check the route

If you look in the FilmExplorer component, you will see that it is already configured to request data from /api/films. Once your endpoint is in place, the main site should load properly with the film data.

We can also test the route directly. Use the pop-out button to put the application on its own page. You can then add /api/films on the end of the URL and you will see the raw film data.

Serve a single film

While FilmExplorer doesn't actually make use of the GET /api/films/:id route, it is instructive to make it anyway (and it can be useful for testing).

Recall that we can create dynamic routes by using square brackets in the name.

Create a new file called [id].js inside of the films directory.

Following the template of index.js, set up a new endpoint for GET.

For the dynamic routes, we need access to the matched variable at the end. Since we called the file [id].js, the variable name will be id. We can extract it from the query with

const { id } = req.query;

Then, you want to get the films array and find the matching film. Use the find. Note that since we got id out of the query, it is a string, so you need to convert it to an integer to compare it to the ids in films. The easiest way to do that is to preface it with the unary + (i.e., +id).

Use the same pattern from above to set the status code and return the film as json.

You can check this out by visiting /api/films/11 (or any other valid id).

Updating the ratings

Our final route will be a PUT request to '/api/films/:id'. This accepts a film with changes (presumably a rating), and uses it to update the local copy.

Add a PUT endpoint after the GET you just completed. This just means chaining the following onto the end of the get:

.put((req, res) => {
    
});

Get the id again.

Since this is a PUT command, the client has sent us the modifications for the film in the body of the request. Because Next.js already applied the body parser middleware, the JavaScript object is parked in req.body:

const updatedFilm = req.body;

Now, you want to merge the new film with the copy we have of the old one and store it back in films. You can use the same map pattern we have used before to create a new list of films with the modified film included.

Send the merged film back in the response object and then call saveData() with your updated list of films to save the changes.

Testing Your Server

There are several ways to test your server, both "informally" and "formally".

"Informal" Testing with curl or the browser

As we showed above, some quick test can be done using the browser. You can also use the command line tool curl.

curl allows you to issue HTTP commands from the command line and will return the result. However, while curl does have the capability of issuing any kind of HTTP request, it quickly ceases to be the quick and easy choice when you are issuing PUT and POST requests.

"Informal" Testing with the Application

Another obvious approach is to launch the application and see if all of its functionality is present.

Of course, this only works in some situations (such as this one where you are handed a fully formed client). In general, this can be problematic as you greatly increase the potential source of errors, so when something does go wrong, it will be difficult to track down what it was.

"Formal" Unit/Integration Testing with Jest

For a more formal approach, you can use Jest along with other libraries to test the routes in a similar fashion.

For this project we added a library called SuperTest, which is used for easily testing HTTP APIs.

Writing Tests

If you look in __tests__/routes.test.js, you will find the start of a collection of tests. You will see our familiar testing pattern, in which you define a test suite, use the beforeEach "setup" function to create a consistent test environment (making the tests "Independent" and "Repeatable"), then execute a set of tests. Each of those tests executes some code, i.e. makes a HTTP request to the API, then makes a set of assertions about the response. In the code below there are examples of using Jest for assertions, and also using the features of the SuperTest library for assertions. Because SuperTest is designed for testing APIs it can be more concise.

You will note that the code in beforeEach has some unfamiliar things in it. Because our routes are buried inside of the Next.js server, we need to be a little clever. In essence, we have to fire up a Next server instance. This uses a bunch of code (in test-utils) that was lifted from the guts of the Next code that they use for internal testing. For the most part you can just use it and not worry about it too much.

Add the final test to check if the parameterized GET route functions properly. You can run the server tests with npm test.

Finishing Up

  1. Add and commit your changes to Github. Make sure to add and commit the new files you created.
  2. Submit your repository to Gradescope

I will expect that:

  • Unparameterized GET request works
  • Parameterized GET request works
  • PUT request works
  • Test for parameterized GET
  • Passes all ESLint checks

Last updated 04/13/2021