35

Node.js TypeScript #7. Creating a server and receiving requests

 5 years ago
source link: https://www.tuicool.com/articles/hit/zEvyieJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In this article, we continue the Node.js series. This time we listen for requests and send  responses .

To do that, we again use the HTPP module from Node.js.

Node.js TypeScript: Creating a server, receiving requests

In the TypeScript Express tutorial , we create a REST API that listens for requests and responds accordingly. While the Express framework is a suitable choice to do that, it adds a layer of abstraction that deals with a lot for us. It very useful, but it might be beneficient to find out how to do it in pure Node.js to understand things better. That said, let’s jump in!

import { createServer, IncomingMessage, ServerResponse } from 'http';
 
const port = 5000;
 
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
  response.end('Hello world!');
});
 
server.listen(port, (error) => {
  if (error) {
    console.log(error);
  } else {
    console.log(`Server listening on port ${port}`);
  }
});

The createServer function returns an instance of an http.Server . One of its prototypes is EventEmitter that we cover in the second part of this series .

If you want to know more about prototypes, check out Prototype. The big bro behind ES6 class

The request event is emitted every time each time a request is sent to our server. The listener that we provide to the  createServer is automatically attached to it.

When we look at our listener, we see two arguments: request and  response . Both of them extend  streams and contain valuable information such as headers, URL that is requested and the HTPP method that is used.

If you want to know more about streams, check out Paused and flowing modes of a readable stream and  Writable streams, pipes, and the process streams

In our simple example, we call the end function on the  response function with some data. It works thanks to the fact, that the response implements a  writable stream.

The server . listen function causes the HTTP server to listen for connections. Now we can start making requests! To do that, I am using Postman here.

jAJBNrI.png!web

Request and Response

The request is an instance of the IncomingMessage . You might remember it from the previous part of the series where we made requests instead of listening for them. One of the core properties it holds is the URL of the request. Thanks to that we can identify what a client wants to do in our application. To specify it more, the request also has a  method . It is one of the HTTP request methods, for example, GET , POST and DELETE . Let’s use this information to add new features to our server.

const posts = [
  {
    title: 'Lorem ipsum',
    content: 'Dolor sit amet'
  }
];
 
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
  switch (request.url) {
    case '/posts': {
      if (request.method === 'GET') {
        response.end(JSON.stringify(posts));
      }
      break;
    }
    default: {
      response.statusCode = 404;
      response.end();
    }
  }
});

Now, when someone tries to access / posts with a GET request, he receives an array of posts. If he attempts to access a URL that we don’t support, we change the statusCode of a response to 404 indicating that the requested resource is not accessible. The  statusCode is one of the properties of a  response object that is an instance of a ServerResponse .

BfYfAzA.png!web

One important thing that might catch your eye is the fact that Postman interprets the data as a regular string because we don’t specify the type. A way to do this is to use HTTP headers . They allow to pass additional information and are attached both to the request , and the response . The one that we need right now is the Content-Type header that indicates the media type of the resource. We want to set it to application/json . To do that, we use the  setHeader function.

interface Post {
  title: string;
  content: string;
}
 
const posts: Post[] = [
  {
    title: 'Lorem ipsum',
    content: 'Dolor sit amet'
  }
];
 
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
  switch (request.url) {
    case '/posts': {
      if (request.method === 'GET') {
        response.setHeader('Content-Type', 'application/json');
        response.end(JSON.stringify(posts));
      }
      break;
    }
    default: {
      response.statusCode = 404;
      response.end();
    }
  }
});

buuiYju.png!web

As you can see at the screenshot above, now our data is correctly identified as JSON. Cool!

When sending requests, we can also add some new data using the POST method. To do that we need to acknowledge the fact that the request is a readable stream. Using the knowledge from the previous part of the course we can create a function that gathers the data from a stream.

const server = createServer((request: IncomingMessage, response: ServerResponse) => {
  switch (request.url) {
    case '/posts': {
      response.setHeader('Content-Type', 'application/json');
      if (request.method === 'GET') {
        response.end(JSON.stringify(posts));
      } else if (request.method === 'POST') {
        getJSONDataFromRequestStream<Post>(request)
          .then(post => {
            posts.push(post);
            response.end(JSON.stringify(post));
          })
      }
      break;
    }
    default: {
      response.statusCode = 404;
      response.end();
    }
  }
});
 
function getJSONDataFromRequestStream<T>(request: IncomingMessage): Promise<T> {
  return new Promise(resolve => {
    const chunks = [];
    request.on('data', (chunk) => {
      chunks.push(chunk);
    });
    request.on('end', () => {
      resolve(
        JSON.parse(
          Buffer.concat(chunks).toString()
        )
      )
    });
  })
}

The getJSONDataFromRequestStream function returns a promise resolved with a data of a generic type . When we finish parsing the data, we add it to the  posts array and return it to the sender.

Uploading files

In the previous part of the series , we upload a photo. Let’s write a server that is capable of handling it!

First, let’s investigate again how does the data that we receive looks like:

const server = createServer((request: IncomingMessage, response: ServerResponse) => {
  switch (request.url) {
    case '/upload': {
      if (request.method === 'POST') {
        const chunks = [];
        request.on('data', (chunk) => {
          chunks.push(chunk);
        });
        request.on('end', () => {
          const result = Buffer.concat(chunks).toString();
          response.end(result);
        });
      }
      break;
    }
    default: {
      response.statusCode = 404;
      response.end();
    }
  }
});

Here we parse the incoming data to a string and send it back.

Mrmmmmj.png!web

As you can see, what we get is a simple string that contains the multipart/form-data.

To know more about multipart/form-data check out Sending HTTP requests, understanding multipart/form-data

We could parse it ourselves, but in this article, we use the multiparty library that can do it for us.

const server = createServer((request: IncomingMessage, response: ServerResponse) => {
  switch (request.url) {
    case '/upload': {
      if (request.method === 'POST') {
        parseTheForm(request);
      }
      break;
    }
    default: {
      response.statusCode = 404;
      response.end();
    }
  }
});
function parseTheForm(request: IncomingMessage) {
  const form = new multiparty.Form();
  form.parse(request);
 
  const fields = new Map();
  let photoBuffer: Buffer;
  let filename: string;
 
  form.on('part', async function(part: multiparty.Part) {
    if (!part.filename) {
      await handleFieldPart(part, fields);
      part.resume();
    }
    if (part.filename) {
      filename = part.filename;
      photoBuffer = await getDataFromStream(part);
    }
  });
 
  form.on('close', () => handleWriting(fields, photoBuffer, filename));
}
async function handleFieldPart(part: multiparty.Part, fields: Map) {
  return getDataFromStream(part)
    .then(value => {
      fields.set(part.name, value.toString());
    })
}
function handleWriting(fields: Map, photoBuffer: Buffer, filename: string) {
  writeFile(
    `files/${fields.get('firstName')}-${fields.get('lastName')}-${filename}`,
    photoBuffer,
    () => {
      console.log(`${fields.get('firstName')} ${fields.get('lastName')} uploaded a file`);
    }
  );
}
function getDataFromStream(stream: Stream): Promise<Buffer> {
  return new Promise(resolve => {
    const chunks = [];
    stream.on('data', (chunk) => {
      chunks.push(chunk);
    });
    stream.on('end', () => {
      resolve(
        Buffer.concat(chunks)
      )
    });
  })
}

In the example above, the multiparty library emits a ‘ part ‘ event every time it creates a stream containing a part of the form. If it is a regular field, we save it in the  fields map. On the other hand, if it is a file, we keep its buffer and filename for later. After we finish the parsing, we save the data in the  files directory.

To handle file upload with Express, we can use the multer library

Summary

In this article, we learned how to create a server and handle incoming requests, including file uploads. The request and  response objects are streams, and therefore we needed to use the knowledge from previous parts of the series. In all of the examples above, there is still quite a lot of error handling that we should do. It makes you appreciate frameworks likeExpresseven more, but the knowledge of how to handle it without it helps in understanding the process.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK