beginner

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 it
  • GET requests has maximum number of characters you can send while POST does not.
  • GET requests data is included in URL and is visible to everyone(through history or web server logs). However, when using POST 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 with GET request and instead you should use POST/PATCH/PUT depending on what you’re trying to do.
  • GET requests can be cached and shared while POSTrequests cannot.
  • GET requests only support string data type while POST 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.

Previous Routes
Next Creating a proper server