HTTP and Express Basics
HTTP Basics
Headers
If we want to provide the metadata about the response we have to provide headers:
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(200, { "content-type": "text/html" });
res.write("<h1>home page</h1>");
res.end();
});
server.listen(5000);
With writeHead
we specify the headers, in our case we specify the status code (200: OK
) and the content type of the response (text/html
). The later are called MIME-types or media types.
Then we specify the body of the response with write
and finally we finalize the message with end
.
Request Object
The request object that is an argument of the createServer
method has several attributes:
req.method
: Allows you to obtain the method of the user’s request, i.e.GET
,POST
,PUT
, etc.req.url
: Contains the url of the user’s request.
HTML File
As we have seen the method write
allows us to define the content of the body as HTML. However we do not need to write the HTML code inside the method we can also pass a file as input and the method will serve it’s content to the response.
const http = require("http");
const { readFileSync } = require("fs");
const homePage = readFileSync("./index.html");
const server = http.createServer((req, res) => {
res.writeHead(200, { "content-type": "text/html" });
res.write(homePage);
res.end();
});
server.listen(5000);
Observe that we user readFileSync
, we do so because, for one this is an example, and also the file is only read once when the server is created, not every time the user hits the server.
External resources
When adding external resources to a given HTML file we also need to handle the request to those resources in our server.
const http = require("http");
const { readFileSync } = require("fs");
const homePage = readFileSync("./index.html");
const homeStyles = readFileSync("./styles.css");
const homeImage = readFileSync("./logo.svg");
const server = http.createServer((req, res) => {
// home page
if (url === "/") {
res.writeHead(200, { "content-type": "text/html" });
res.write(homePage);
res.end();
}
// styles
else if (url === "/styles.css") {
res.writeHead(200, { "content-type": "text/css" });
res.write(homeStyles);
res.end();
}
// image/logo
else if (url === "/logo.svg") {
res.writeHead(200, { "content-type": "image/svg+xml" });
res.write(homeImage);
res.end();
}
});
Note that the content types differ every time, with css we use text/css
, with images we use image/svg+xml
.
Express
Initializing Express App
In order to do so we import the express
module, and the we create the instance, more or less like we did with our HTTP servers:
const express = require("express");
const app = express();
App Methods
The app
instance we just created has several methods, we now list the most common:
app.get
: HTTP method to read data.
app.get("/", (req, res) => {
res.status(200).send("Home Page");
});
app.post
: HTTP method to insert data.app.put
: HTTP method to update data.app.delete
: HTTP method to delete data.app.all
: Usually used to respond when we cannot locate a resource on the server.
app.all("*", (req, res) => {
res.status(404).send("<h1>resource not found</h1>");
});
app.use
: It is responsible for the middleware.app.listen
: This method listens for any requests made to the server.
app.listen(5000, () => {
console.log("server is listening on port 5000...");
});
Send HTML files
To send HTML files as a response instead of plain text we have to use the sendFile
method:
const express = require("express");
const path = require("path");
const app = express();
app.get("/", (req, res) => {
res.sendFile(path.resolve(__dirname, "./index.html"));
});
app.listen(5000, () => {
console.log("server is listening on port 5000...");
});
Now, we have to import the external resources needed by the HTML file:
const express = require("express");
const path = require("path");
const app = express();
app.use(express.static("./public"));
app.get("/", (req, res) => {
res.sendFile(path.resolve(__dirname, "./index.html"));
});
app.listen(5000, () => {
console.log("server is listening on port 5000...");
});
So we invoke app.use
as to tell the server that there are static resources stored in the public
folder.
However, because in this case index.html
is also a static file we can remove the sendFile
method if we store index.html
inside the public
folder:
const express = require("express");
const path = require("path");
const app = express();
app.use(express.static("./public"));
app.listen(5000, () => {
console.log("server is listening on port 5000...");
});
HTTP Methods
GET
app.get("/api/people", (res, req) => {
res.status(200).json({ success: true, data: people });
});
POST
Observe that we use a middleware provided by Express
that lets us parse incoming requests with urlencoded
payload, and another middleware function to parse json
.
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.post("/api/people", (res, req) => {
const { name } = req.body;
if (!name) {
return res
.status(400)
.json({ success: false, msg: "Please provide a name" });
}
// Send array of people adding the new person (this is not permanent)
res.status(201).json({
success: true,
data: [...data, { name, id: data.length + 1 }],
});
});
PUT
app.put("/api/people/:id", (res, req) => {
// De-structure params
const { id } = req.params;
const { name } = req.body;
const person = people.find((person) => person.id === Number(id));
// The person does not exist
if (!person) {
return res
.status(400)
.json({ success: false, msg: `no person with id: ${id}` });
}
// Update the person data
const newPeople = people.map((person) => {
if (person.id === Number(id)) {
person.name = name;
}
return person;
});
res.status(200).json({ success: true, data: newPeople });
});
DELETE
app.delete("/api/people/:id", (res, req) => {
// De-structure params
const { id } = req.params;
const { name } = req.body;
const person = people.find((person) => person.id === Number(id));
// The person does not exist
if (!person) {
return res
.status(400)
.json({ success: false, msg: `no person with id: ${id}` });
}
// Filter the person data
const newPeople = people.filter((person) => person.id !== id);
res.status(200).json({ success: true, data: newPeople });
});
Routes
Set Up
In order to set up the routes for our project, we first create a folder called routes
that will contain all the javascript
files that control routing functionality. In this example we create two files within routes
, people.js
and auth.js
.
Once we have created them, we include them as middleware to the specific endpoints (/api/people
for people.js
and /login
for auth.js
), as follows:
const express = require("express");
const app = express();
const people = require("./routes/people");
const auth = require("./routes/auth");
app.use("/api/people", people);
app.use("/login", auth);
app.listen(5000, () => {
console.log("Server is listening on port 5000....");
});
Router
Let’s focus now on people.js
than controls the routing of /api/people
. For that we import the controller of this endpoint and we specify the functions to execute for the different HTTP
methods and for the different routes.
/
: This is the default endpoint/api/people
there we specify that the logic for a get request is contained in thegetPeople
function./:d
: This endpoint allows for specifying an id as a parameter.
const express = require("express");
const router = express.Router();
const {
getPeople,
createPerson,
createPersonPostman,
updatePerson,
deletePerson,
} = require("../controllers/people");
router.route("/").get(getPeople).post(createPerson);
router.route("/:id").put(updatePerson).delete(deletePerson);
module.exports = router;
Controller
The people controller contains:
let { people } = require("../data");
const getPeople = (req, res) => {
res.status(200).json({ success: true, data: people });
};
const createPerson = (req, res) => {
const { name } = req.body;
if (!name) {
return res
.status(400)
.json({ success: false, msg: "please provide name value" });
}
res.status(201).send({ success: true, person: name });
};
const createPersonPostman = (req, res) => {
const { name } = req.body;
if (!name) {
return res
.status(400)
.json({ success: false, msg: "please provide name value" });
}
res.status(201).send({ success: true, data: [...people, name] });
};
const updatePerson = (req, res) => {
const { id } = req.params;
const { name } = req.body;
const person = people.find((person) => person.id === Number(id));
if (!person) {
return res
.status(404)
.json({ success: false, msg: `no person with id ${id}` });
}
const newPeople = people.map((person) => {
if (person.id === Number(id)) {
person.name = name;
}
return person;
});
res.status(200).json({ success: true, data: newPeople });
};
const deletePerson = (req, res) => {
const person = people.find((person) => person.id === Number(req.params.id));
if (!person) {
return res
.status(404)
.json({ success: false, msg: `no person with id ${req.params.id}` });
}
const newPeople = people.filter(
(person) => person.id !== Number(req.params.id)
);
return res.status(200).json({ success: true, data: newPeople });
};
module.exports = {
getPeople,
createPerson,
createPersonPostman,
updatePerson,
deletePerson,
};
Route Params
If, for example, we have a list of products, and we want to get a certain product by its id, we use route params
. They can have any name, and are specified by :param
. This is then stored in the request
object.
app.get("/api/products/:productID", (req, res) => {
// De-structure param
const { productID } = req.params;
// Filter products by id
const singleProduct = products.find(
(product) => product.id === Number(productID)
);
// If it does not exist
if (!singleProduct) {
return res.status(404).send("Product Does Not Exist");
}
return res.json(singleProduct);
});
Note that the route params
are always strings, in our case we had to convert it to a Number
. We can also have more that one route parameter
like so:
app.get("/api/products/:productID/reviews/:reviewID", (req, res) => {
res.send("hello world");
});
Where we define productID
and reviewID
as route parameters, and can, therefore, filter by them.
Query Strings
We can use the query
attribute from the request
object in order to further filter our data. So whenever the user types localhost:5000/whateverendpoint?name=john
, the request
object passed as an argument of the callback defined for whateverendpoint
will have the object {name: 'john'}
stored in request.query
.
app.get("/whateverendpoint", (req, res) => {
console.log(req.query);
});
Now we code the way to filter by the keywords search
and limit
:
app.get("/api/v1/query", (req, res) => {
// De-structure keys
const { search, limit } = req.query;
// Get a copy of the products
let sortedProducts = [...products];
// If search was specified
if (search) {
// Return only the products whose name start with
sortedProducts = sortedProducts.filter((product) => {
return product.name.startsWith(search);
});
}
// If limit was specified
if (limit) {
// Return as many products as the limit specified
sortedProducts = sortedProducts.slice(0, Number(limit));
}
// If no product matched the search
if (sortedProducts.length < 1) {
return res.status(200).json({ sucess: true, data: [] });
}
// Return the products filtered
res.status(200).json(sortedProducts);
});
So now, if we go to localhost:5000/api/v1/query?search=a&limit=2
the server will return a JSON
object that contains at most 2 products whose name start with an “a”.
Observe, that in order to avoid error for sending more than one response (note that we have two res.json()
in our function), we must add the return
keyword after sending each response, then the method exits.