HTTP request methods | Events and streams
You now know how to use routes in order to serve different content based on different paths. But it only works for GET requests. Why is that?
HTTP Request Methods
HTTP request methods define the actions a client wants to perform on a particular resource. We have multiple methods:
- GET: Retrieve data from a specified resource.
- POST: Submit data to be processed to a specified resource.
- PUT: Update a resource or create a new resource if it does not exist.
- DELETE: Delete a specified resource.
- PATCH: Apply partial modifications to a resource.
- HEAD: Retrieve the headers of a resource, used to check for changes without retrieving the resource.
- CONNECT: Establishes a tunnel to the server identified by the target resource.
- OPTIONS: Describes the communication options for the target resource.
- TRACE: Performs a message loop-back test along the path to the target resource.
In 99% of usecases you will use GET
, POST
, PATCH
or PUT
and DELETE
.
Difference between PATCH
and PUT
Both are used to update a resource but in a different way. Generally, PUT
should be used to update entire existing resource(meaning it requires all properties to be sent) or (less known) to “replace or create a resource” while PATCH
should be used to partially update existing resource(meaning only some properties need to be sent).
GET
vs POST
While it is possible to use only GET
for all your requests you should never do it. Here is why:
GET
should only be used to retreive data from server and not modify itGET
requests has maximum number of characters you can send whilePOST
does not.GET
requests data is included in URL and is visible to everyone(through history or web server logs). However, when usingPOST
your data is not part of URL and hence not visible to anyone(it is not stored in history or web server logs). This means that all your private informations like passwords and such should never be sent withGET
request and instead you should usePOST
/PATCH
/PUT
depending on what you’re trying to do.GET
requests can be cached and shared whilePOST
requests cannot.GET
requests only support string data type whilePOST
supports multiple data types.
Query Parameters vs Message Body
Both are used for carrying data from the client to the server but they do it in different ways. Query parameters are part of URL (e.g. www.mariodoesdev.com/posts?size=10&page=5
, everything after ?
is considered query parameter(s)
). On the other hand, message body
is not a part of URL and is sent through specific ways with terminal or special applications(mentioned later). While a message body can be a part of all request methods it is usually done only for POST
, PATCH
, PUT
while query parameters is used for all request methods and alongside message body in some scenarios for POST
, PATCH
, PUT
.
Additional Info
Whenever you type in a url in your browser you are doing a GET request. For other types of requests you need to use terminal(with something like cUrl) or Bruno
With our new knowledge on HTTP request methods we can now continue with improving our routes and handling multiple methods. The following example is going to be a bit more complex due to how NodeJS handles request body but don’t worry, everything will be explained in details(for beginners).
Improving Our Code
const http = require("http");
function handleUnsupportedMethod(res) {
res.writeHead(405, { "Content-Type": "text/plain" });
res.end("Method Not Allowed");
}
function handlePostMethod(req, res) {
let combined = "";
// Handle incoming data in chunks
req.on("data", (chunk) => {
combined += chunk;
});
req.on("end", () => {
try {
// Send a response
res.writeHead(200, { "Content-Type": "application/json" });
res.end(`You sent this data: ${combined}`);
} catch (error) {
// Handle parsing error
res.writeHead(400, { "Content-Type": "application/json" });
res.end("Invalid received data.");
}
});
}
function router(req, res) {
const protocol = req.protocol ?? "http";
const baseUrl = `${protocol}://${req.headers.host}/`;
const reqUrl = new URL(req.url, baseUrl);
const path = reqUrl.pathname;
switch (path) {
case "/":
if (req.method !== "GET") {
return handleUnsupportedMethod(res);
}
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Welcome to the Home Page!");
break;
case "/about":
if (req.method !== "GET") {
return handleUnsupportedMethod(res);
}
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("This is the About Page!");
break;
case "/posts":
if (req.method !== "POST") {
return handleUnsupportedMethod(res);
}
return handlePostMethod(req, res);
break;
default:
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 Not Found");
break;
}
}
const server = http.createServer(router);
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Understanding Our Code
As I said, a lot is going on in the code so let’s start. As we know, this is the “main” part of our code:
const server = http.createServer(router);
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
We are using router
variable in our built-in createServer
function. Our router
function is defined by us and we have req
and res
that is being passed internally to our function(⏭️).We are also handling our url and pathname as described in routes
function router(req, res) {
const protocol = req.protocol ?? 'http';
const baseUrl = `${protocol}://${req.headers.host}/`;
const reqUrl = new URL(req.url, baseUrl);
const path = reqUrl.pathname;
...
}
Then we are handling our endpoints
by using a switch
statement. But now we need to differentiate which endpoints are for which HTTP method so after we match our switch case to an endpoint we are checking for request method and if its not the expected method we call handleUnsupportedMethod
defined by us, where we just return 405 status code indicating that desired method is not supported for that specific endpoint. But if we do get the method we are expecting, then we handle our request as intended.
...
function handleUnsupportedMethod(res) {
res.writeHead(405, { 'Content-Type': 'text/plain' });
res.end('Method Not Allowed');
}
...
...
switch (path) {
case '/':
if (req.method !== 'GET') {
return handleUnsupportedMethod(res);
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Welcome to the Home Page!');
break;
...
In the above code we actually don’t need a return
when calling handleUnsupportedMethod
because in that function we are calling res.end
which immediately sends back the response. I included it because it makes more logical sense in the code because otherwise we wouldn’t be sure if the code execution is going forward without having to look into handleUnsupportedMethod
.
if (req.method !== "GET") {
return handleUnsupportedMethod(res);
}
Now moving onto actually handling the POST
request and body
of the request. We are doing this by calling handlePostMethod
and passing in required req
and res
(because we are using them in the function). Again, return
is not needed but it makes your life a lot easier when going through the code later.
To understand what is happening next, we need to understand basics of events and streams in NodeJS.
Events and Streams
These are really interesting but also complex topics(also base for other topics) so for now(for beginners) I’m going to keep it very simplified.
Imagine you have a book of 500 pages and it has 900k characters in total. If you were to get all of that information(900k characters) all at once in your brain it would be hard to make sense of and it would overwhelm you. So you go page by page and read it slowly. In our example, the book would be a stream
of data and each page would be a chunk
of that data. Now imagine you get a page of a book every few days and you want to combine it into a book. You don’t know when a page is going to arrive so you need to wait. In this example, each time a page arrives is considered an event
.
So in our code snippet we create a variable combined
and then, because req
is actually a stream
it has a built-in on
function, we use the on
function to listen for events but not just any event but for a specific event with the internally predefined name data
. There are many built-in event name alongside data
(⏭️). Whenever we get that event, we also get a new chunk
of data that we handle with our arrow function
and combine that chunk with other, already received chunks.
...
let combined = '';
// Handle incoming data in chunks
req.on('data', (chunk) => {
combined += chunk;
});
...
While we are getting data
events that means that we still can expect more events from this stream. Only when we receive end
event it means that no more data will be available from that stream. In our example we send a response back only when we receive that event because only then we know we combined every chunk from that stream. If for any reason we encounter any error we send a response saying that we received invalid data and with 400 status code indicating its a client problem.
...
// All data has been received
req.on('end', () => {
try {
// Send a response
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(`You sent this data: ${combined}`);
} catch (error) {
// Handle parsing error
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end('Invalid received data.');
}
});
...
Send a POST Request
All of this code for what? To be honest, not much, just a simple string response back, try it yourself. You can open your terminal (or use already mentioned Insomnia or Postman) and send a POST request to your server. Here is an example:
Terminal:
curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"john@example.com"}' http://localhost:3000/posts
or PowerShell(Windows):
curl -Method POST -Uri http://localhost:3000/posts -ContentType 'application/json' -Body '{"name":"John Doe","email":"john@example.com"}'
and the response is:
You sent this data: {"name":"John Doe","email":"john@example.com"}
If you want to know more about the command I sent:
curl
- name of command
-X POST
- type of HTTP request method
-H "Content-Type: application/json"
- defining headers (only one in our case Content-Type
)
-d '{"name":"John Doe","email":"john@example.com"}'
- our request body
http://localhost:3000/posts
- url that we want to send the request to
What Now?
Now you are ready to create a simple API, congratulations. And you know what a stream and an event is and this is very important when working with NodeJS and programming in general. You’ll see that things get complicated quickly as you add more and more endpoints so in the next few posts we are going to organize it better.