MIT Weblab笔记 - APIs and Promises Introduction

Introduction

  • Client sends request to server, and server responds with result.

  • A server binds to a port on a computer. A computer can run multiple servers simultaneously, each server binds to a unique port.

  • Domain of my own computer: localhost. (http://localhost:3000)

APIs

  • API (Application Programming Interface). Interface can be thought of as a contract of service between two applications. This contract defines how the two communicate with each other using requests and responses.
  • Frontend makes http requests to the endpoints defined on the backend.

HTTP(S) Methods

  • GET: Retrieves data from the server.
  • POST: Sends data to the server, often causing a change or creation of new data.
  • PUT: Replaces data on the server.
  • DELETE: Deletes data on the server.

HTTP Request

  • Request Target (URL) + Params: The endpoint and any parameters.
  • HTTP Method: The type of action (GET, POST, etc.).
  • Headers: Additional metadata for the request.
  • Body: The data sent with the request (for POST and PUT).

HTTP Response

  • Status Code: Indicates the result of the request.
    • 2xx: Success
    • 4xx: Client errors
    • 5xx: Server errors
  • Headers: Metadata about the response.
  • Body: The data returned by the server.

Make HTTP Requests

The fetch() function in JavaScript is used to make network requests.

1
fetch(url, options)
  • url: he URL to which the request is sent.
    • absolute url: e.g. http://example.com/api/data
    • relative url: e.g. ‘api/data’ (relative to current page), ‘../api/data’, ‘/api/data’ (relative to root)
  • options: the option object including properties like method, headers, body, mode

Get Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Helper code to make a get request. Default parameter of empty JSON Object for params.
// Returns a Promise to a JSON Object.
export function get(endpoint, params = {}) {
const fullPath = endpoint + "?" + formatParams(params);
return fetch(fullPath)
.then(convertToJSON)
.catch((error) => {
// give a useful error message
throw `GET request to ${fullPath} failed with error:\n${error}`;
});
}

// In front-end:
get("/api/comment", { parent: props._id }) // send the request
  • Parameters are passed as part of the URL in the query string for get request

Post Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Helper code to make a post request. Default parameter of empty JSON Object for params.
// Returns a Promise to a JSON Object.
export function post(endpoint, params = {}) {
return fetch(endpoint, {
method: "post",
headers: { "Content-type": "application/json" },
body: JSON.stringify(params),
})
.then(convertToJSON) // convert result to JSON object
.catch((error) => {
// give a useful error message
throw `POST request to ${endpoint} failed with error:\n${error}`;
});
}

// In front-end:
const body = { content: value };
post("api/story", body) // send the request
  • Parameters are included in the request body as key-value pairs. The body can be in various formats (e.g., JSON, XML, form data), but JSON is common.
  • body: The actual data sent with the request. JSON.stringify(params) converts the JavaScript object params into a JSON string.

Define API Endpoints

The following uses Express.js

When the backend receives a request to an API endpoint, it executes the corresponding route handler function to process the request, interact with the database, and send back a response.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const app = express();
// get
app.get("/api/stories", (req, res) => {
res.send(data.stories);
});
// post
app.post("/api/story", (req, res) => {
const newStory = {
_id: data.stories.length,
creator_name: myName,
content: req.body.content,
};

data.stories.push(newStory);
res.send(newStory);
});
  • req: The req object represents the HTTP request and contains information about the request that was made by the client. It can be used to access information about the incoming request.
    • req.query: Extract the query parameters (for get request) from the query string in the URL
    • req.body: Data sent in the body of the request (typically with POST, PUT, or DELETE requests).
  • res: The res object represents the HTTP response that an Express application sends when it receives a request.
    • res.send(): The parameter can be of different types, such as a string, Buffer, JSON object, array, etc.
    • res.status(404).send(‘Page not found’): First set status code and then send.
    • res.sendFile(path.join(reactPath, “index.html”)): send a file back

Middleware

Middleware: Run code in between receiving a request and running endpoint code. Middlewares are called in order of definition. Register middleware by calling app.use().

1
2
const app = express();
app.use(path, middlewareFunction);
  • path (optinoal): Specifies the base path for the middleware function. If omitted, the middleware will be executed for every request to the app.
  • A function to execute for the given path.

Using Middleware to route

1
2
3
4
5
6
7
8
9
// api.js
const express = require('express');
const router = express.Router();

router.get('/test', (req, res) => {
res.send('API Test Endpoint');
});

module.exports = router;
1
2
3
4
5
6
7
8
9
10
11
// server.js
const express = require('express');
const app = express();
const apiRouter = require('./api');

app.use(express.json()); // Middleware to parse JSON bodies
app.use('/api', apiRouter); // Middleware to route /api paths to api.js

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

When http://localhost:3000/api/test request is sent to the server, the following sequence happens:

  1. The JSON parsing middleware express.json() processes the request.
  2. The request path /api/test is checked against the apiRouter, matches, and then the request is forwarded to apiRouter.
  3. The router checks its defined routes and finds a match for GET /test, and the handler for /test is executed.

Promises

Async Handling

Get or Post returns a promise. Promises allow for asynchronous processing.

A bad example:

1
2
const myStories = get('/api/stories');
setStories(myStories);

In this example, get('/api/stories') returns a promise, but setStories(myStories) is called immediately, before the promise has resolved. As a result, the immediately returned promise: myStories, will not contain any data.

Correct way to handle this:

1
2
3
4
5
6
7
get('/api/stories')
.then(data => {
setStories(data); // Set the stories with the fetched data
})
.catch(error => {
console.error('Error fetching stories:', error); // Handle any errors
});
  • .then() block waits for the promise to resolve. Once the data is fetched, the parsed data is then passed to setStories(data). This also returns a promise.
  • The .catch() block handles any errors. Once the promise is rejected, do stuff (call a callback function). Returns a promise.
  • If the fetch is accepted, .catch() will not be executed. If the fetch is rejected, any .then() will not be executed.

Put Together

Get request example:

1
2
3
4
5
6
7
8
9
10
11
// In frontend:
get("/api/comment", { parent: props._id }) // send a get request to backend
.then((comments) => {
setComments(comments);
});

// In backend:
router.get("/comment", (req, res) => { // define the logic to handle the get request from url "/api/comment"
const filteredComments = data.comments.filter((comment) => comment.parent == req.query.parent);
res.send(filteredComments); // send responses back to the client
});

The following happens sequentially:

  1. The get function sends a GET request to the backend at the endpoint “/api/comment” with a query parameter parent set to props._id.
  2. The backend’s router.get("/comment", (req, res) => { ... }) function is triggered by the incoming GET request to “/comment”.
  3. The backend extracts the parent parameter from the query string using req.query.parent.
  4. The backend constructs and sends the filteredComments array back to the frontend using res.send(filteredComments).
  5. The frontend’s promise, returned by the get function, resolves with the filteredComments array received from the backend.
  6. The .then((comments) => { ... }) method is called with comments as the argument, which is the filteredComments array.
  7. setComments(comments) is called, triggering a re-render of the component with the new comments.

Put request example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// In frontend:
const body = { parent: props.storyId, content: value };
post("/api/comment", body) // send a get request to backend
.then((comment) => {
props.addNewComment(comment);
});

// In backend:
router.post("/comment", (req, res) => {
const newComment = {
_id: data.comments.length,
creator_name: myName,
parent: req.body.parent,
content: req.body.content,
};

data.comments.push(newComment);
res.send(newComment);
});
  • The parameter of post request can be extracted by req.body.xxx in the backend.
  • The backend sends back the newComment to the client.
  • .then() waits for the promise to resolve. The callback function defined in .then() takes the response as the parameter and makes changes.

MIT Weblab笔记 - APIs and Promises Introduction
https://thiefcat.github.io/2024/07/05/MIT-Weblab/APIs-Promises/
Author
小贼猫
Posted on
July 5, 2024
Licensed under