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.
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
// 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
// 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.
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().
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
// api.js
const express = require('express');
const router = express.Router();
router.get('/test', (req, res) => {
res.send('API Test Endpoint');
});
module.exports = router;
// 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:
- The JSON parsing middleware
express.json()processes the request. - The request path
/api/testis checked against theapiRouter, matches, and then the request is forwarded toapiRouter. - The router checks its defined routes and finds a match for GET
/test, and the handler for/testis executed.
Promises
Async Handling
Get or Post returns a promise. Promises allow for asynchronous processing.
A bad example:
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:
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 parseddatais then passed tosetStories(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:
// 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:
- The
getfunction sends a GET request to the backend at the endpoint “/api/comment” with a query parameter parent set to props._id. - The backend’s
router.get("/comment", (req, res) => { ... })function is triggered by the incoming GET request to “/comment”. - The backend extracts the parent parameter from the query string using
req.query.parent. - The backend constructs and sends the
filteredCommentsarray back to the frontend usingres.send(filteredComments). - The frontend’s promise, returned by the
getfunction, resolves with thefilteredCommentsarray received from the backend. - The
.then((comments) => { ... })method is called withcommentsas the argument, which is thefilteredCommentsarray. - setComments(comments) is called, triggering a re-render of the component with the new comments.
Put request example:
// 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.xxxin 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.