Week 8 - Working with Remote Data - CI256

Week 8 - Working with Remote Data

Types of Requests

Main types of HTTP request:

  • GET - retrieve data
  • POST - create data
  • PATCH - update data
  • PUT - replace data
  • DELETE - delete data

Each of these represent a different “VERB” or type of action on the server.

Note: The difference between PUT and POST is that PUT is idempotent: calling it once is no different from calling it several times successively (there are no side effects). Successive identical POST requests may have additional effects, such as creating the same order several times.

REST APIs

REST APIs are the most common API type, and comprised of many url paths, or “endpoints”.

They are typically structured so that their URLs represent a hierarchy from left to right.

  • POST /token - Retrieve a login token
  • GET /contacts - Get a list of contacts
  • GET /contacts/{contact_id} - Get a specific contact
    • Returns a single object instead of a list, and often returns more details than a list endpoint
  • GET /contacts/{contact_id}/profile - Get a specific user’s profile image
    • Note how /profile could be seen as “nested under” a specific contact’s path
  • POST /contacts - Create a new contact
  • PATCH /contacts/{contact_id} - Update an existing contact
  • DELETE /contacts/{contact_id} - Delete an existing contact

Some other common examples of nesting might be:

  • GET /groups/{group_id}/users: get all users from a given group
  • POST /groups/{group_id}/activities/{activity_id}/start to start a group activity

Parameters

Some requests will accept additional parameters, affecting how they operate. Some parameters will be option while others will be required.

Query Parameters

Any request type can accept query parameters, though it’s most commonly used with GET requests.

GET requests can only receive query parameters.

The “query string” starts at the end of a URL with the ? character, and is followed by a series of key/value pairs separated by & characters.

                                                   val    val
                    key      val  key   val   key  ||  key |
               vvvvvvvvvvvv vvvv vvvvv  vvvv vvvvv vv vvvv v
GET /contacts/?public=true&active=true&count=50&page=2

If we translated that set of key/value pairs to an object, we would have:

{
  "public_users": true,
  "active": true,
  "count": 50,
  "page": 2
}

Contacts REST API

CI256 has a REST API set up at https://api.ci256.cloud for maintaining a list of Contacts.

Built-in Documentation

It is common for many REST APIs to contain “OpenAPI” documentation. Multiple tools exist for displaying the docs in the format, such as Swagger and ReDoc.

Swagger: https://api.ci256.cloud/docs

Redoc: https://api.ci256.cloud/redoc

GET /contacts

Returns all contacts in a list. Does not include extra fields

Output:

[
  {
    "id": 3,
    "firstName": "Taylor",
    "lastName": "Swift",
    "email": "[email protected]",
    "phone": "615-555-1989"
  },
  {
    "id": 4,
    "firstName": "Pedro",
    "lastName": "Pascal",
    "email": "[email protected]",
    "phone": "512-555-8765"
  },
]
GET /contacts/{contact_id}

Returns a single contact object, including the extra field.

Output:

{
  "lastName": "Pascal",
  "id": 4,
  "firstName": "Pedro",
  "phone": "512-555-8765",
  "email": "[email protected]",
  "extra": {
    "address": "456 Hollywood Blvd, Los Angeles, CA 90028",
    "birthday": "04/02/1975",
    "assistant": "Maria Lopez"
  }
}

Basic Authentication / Authorization

APIs will generally require authentication for use - otherwise anyone could do anything, and that’s bad!

All HTTP requests support “Headers” as a way to send information about the request. The Authorization header is typically used to define “who” is sending the request.

The Authorization header is usually split into two fields - the scheme and the credential. Some common schemes include Basic and Bearer, but custom scheme’s like apikey or owl can also be used. There are really not rules here - just “common things” in the industry.

The Contacts API has two types of contacts - public, which cannot be modified, and private, which can only be viewed/modified by an authenticated user.

Authenticating Via API Key

To authenticate with the Contacts API using an API key, use apikey as the Authorization Schema. That means your header will look like this:

{
  "Authorization": "apikey TOKEN_HERE"
}

For testing, you may replace TOKEN_HERE with ci256.

For your final project, you should replace TOKEN_HERE with the password given to you for the Git Server.

Testing Authentication

A common functionality of APIs is to have a /me endpoint or something similar for testing authentication.

You can make a request to /me and you will get your username back if successful, otherwise, you will get an HTTP error code.

curl --silent 'https://api.ci256.cloud/me' -H 'Authorization: apikey ci256'
"test"

$ curl --silent 'https://api.ci256.cloud/me' -H 'Authorization: apikey INVALID' 
{"detail":"user is not logged in"}

Unauthenticated endpoints

The endpoints GET /contacts, GET /contacts/{contact_id}, and POST /token are the only endpoints which can be used without authentication. They will only return public users when not authenticated.

This is meant as a testing step on your way to authenticated access.

Other Types of APIs

GraphQL is a new type of API becoming more popular in recent years. It allows you to query fields individually, rather than being stuck with what the REST API defines as a response.

JS Fetch API

We will be using the Javascript fetch API to make REST API requests.

We used this during week 3 to fetch data from JSON placeholder.

Here is an example of using it to fetch the list of contacts with our special testing token:

async function testApi(apiToken) {
    // create new fetch request and await for the response
	const response = await fetch("https://api.ci256.cloud/me", {
		// include the authorization header
		headers: {"Authorization": `ApiKey ${apiToken}`}
	});

	// check if the request had a successful error code
    if (!response.ok) {
      console.error(`Response status: ${response.status}`);
      throw new Error(`Response status: ${response.status}`)
    }

	// retrieve the actual data
	// note that .json() is async and must be awaited
    const data = await response.json();
    // debug logs FTW!
    console.log(`got identity: "${data}"`)
    // return the data
    return data;
}

// call the function with our test token
testApi("ci256")

HTTP Response Codes

An HTTP response will always contain a Response Code to report if the request was successful or not. Each range of response codes means a special thing:

  1. Informational responses (100 – 199)
  2. Successful responses (200 – 299)
  3. Redirection messages (300 – 399)
  4. Client error responses (400 – 499)
  5. Server error responses (500 – 599)

The most common error codes to know are:

  • 200/201: success!
  • 400: Bad request - check your inputs, you probably sent something incorrectly
  • 401/403: invalid authentication
  • 500: Server error, probably not your fault!

The status of HTTP responses can be found on the fetch object, but also in the browser debugger “Network” tab. This tab has a ton of useful information!

You can even see the raw data returned from the API endpoint

Lab - Final Project Remote Data

For Lab, you’ll be working on adding remote data access to your final project.

1. Load Public Contacts

First, we’ll want to convert our application to load the contact list from the remote API. We can use a loader function at the layout level to load data once into our application.

Component: App,  
loader: async ({params}) => {  
  // load contact list here
},  
children: [

Then inside of our <App /> component, we use that data via useLoaderData() instead of importing our static contact list.

- import contacts from './contacts'

 function App() {  
+  const contacts = useLoaderData();  

You’ll know it’s working if:

  • Your sidebar shows a bunch of contact names
  • If your final project is up to date, clicking on a contact results in the error `contact not found
    • This is because our new contact IDs start at 1000, and our old contact IDs are 1-100

2. Load Contact Details

Now that we’re loading the updated list of contacts from the API, lets update our /users/:id loader function to load live API data.

If your loader function looks like this from last week’s lab:

{
	path: "/users/:id",
	element: <DisplayContact />,
	// pass a loader function to load our contact data for us
	loader: async ({params}) => {
	  // params is an object with our :id argument as a string. 
	  // Ex, if we loaded the path /users/3 the params object would be
	  // { id: '3' }
	  console.log('loading contact with id', params.id)
	  // we call a custom getContact() function with the ID
	  return await getContact(params.id)
	}

Then you can simple update the getContact function!

Instead of something like this:

export async function getContact(id: string): Promise<Contact | undefined> {  
  return contacts.find(...);  
}

We’ll want to have something like this:

export async function getContact(id: string): Promise<Contact | undefined> {  
  const response = await fetch(`https://api.ci256.cloud/...`);
  
  // check if the request had a successful error code  
  if (!response.ok) {  
    console.error(`Response status: ${response.status}`);  
    throw new Error(`Response status: ${response.status}`)  
  }  
  
  // retrieve the actual data  
  // note that .json() is async and must be awaited
  const data = await response.json();  
  // debug logs FTW!  
  console.log(`got contacts`, data)  
  // return the data  
  return data;  
}

3. Deleting the Contacts Array

At this point, we should be able to delete the entire contacts array from our code. Try that, and be sure to clean up anywhere it is imported.

Code cleanup! Excite!

4. Implementing Authentication

Lets implement some basic authentication to our application using our API key.

Note: It’s generally a very bad idea to store an API key directly in code, but it’s fine for this class.

Add the Authorization Header with an API Key value of ci256 for now.

const response = await fetch("https://api.ci256.cloud/contacts", {  
  // include the authorization header  
  headers: {"Authorization": `ApiKey ci256`}  
});

Be sure to include this header with all requests!

You’ll know if it’s working if:

  • You get a test user at the top of the sidebar list
  • You can load the user with id 0 by clicking on that user in the sidebar

5. Replace Credentials

Replace the test ci256 API key with the password you were provided for the git server.

You’ll know if this is working because you will now see your username in the sidebar, and when you click on your own user.