- 1. Node.js TypeScript #1. Modules, process arguments, basics of the File System
- 2. Node.js TypeScript #2. The synchronous nature of the EventEmitter
- 3. Node.js TypeScript #3. Explaining the Buffer
- 4. Node.js TypeScript #4. Paused and flowing modes of a readable stream
- 5. Node.js TypeScript #5. Writable streams, pipes, and the process streams
- 6. Node.js TypeScript #6. Sending HTTP requests, understanding multipart/form-data
- 7. Node.js TypeScript #7. Creating a server and receiving requests
- 8. Node.js TypeScript #8. Implementing HTTPS with our own OpenSSL certificate
- 9. Node.js TypeScript #9. The Event Loop in Node.js
- 10. Node.js TypeScript #10. Is Node.js single-threaded? Creating child processes
- 11. Node.js TypeScript #11. Harnessing the power of many processes using a cluster
- 12. Node.js TypeScript #15. Benefits of the HTTP/2 protocol
- 13. Node.js TypeScript #12. Introduction to Worker Threads with TypeScript
- 14. Node.js TypeScript #13. Sending data between Worker Threads
- 15. Node.js TypeScript #14. Measuring processes & worker threads with Performance Hooks
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!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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(); } } }); |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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(); } } }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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)); } |
1 2 3 4 5 6 |
async function handleFieldPart(part: multiparty.Part, fields: Map) { return getDataFromStream(part) .then(value => { fields.set(part.name, value.toString()); }) } |
1 2 3 4 5 6 7 8 9 |
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`); } ); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 like Express even more, but the knowledge of how to handle it without it helps in understanding the process.
It would be great to make an article how to build this stuff with tsc or webpack, cause if you want to run your code on real hosting you will need .js files.
server.listen(port, (error) => {
if (error) {
console.log(error);
} else {
console.log(
</span><span style="color: rgb(246, 102, 16);">Server</span><span style="color: rgb(0, 111, 224);"> </span><span style="color: rgb(246, 102, 16);">listening</span><span style="color: rgb(0, 111, 224);"> </span><span style="color: rgb(246, 102, 16);">on</span><span style="color: rgb(0, 111, 224);"> </span><span style="color: rgb(246, 102, 16);">port</span><span style="color: rgb(0, 111, 224);"> </span><span style="color: rgb(51, 51, 51);">${</span><span style="color: rgb(172, 76, 99);">port</span><span style="color: rgb(51, 51, 51);">}
);}
});
It will be nice if you can refactor or state this method “listen” on server doesn’t take argument as part of the callback