My aim with this post is to explain building a plain REST API without using any fancy framework like Express.
Enjoy! โ๏ธ
Prerequisites
The web follows a client-server model. It comprises two kinds of machines (or programs), namely, the service provider (server) and the service requester (client). The interaction between a client and a server is dictated by a protocol, a set of rules, called HTTP.
Some key points about the model:
A server listens for (awaits) requests from clients. When a client sends a request, the server processes it and sends an appropriate response back to the client. This is called a request-response cycle.
There cannot be multiple responses to a single request.
A response always comes with a status code that indicates whether the request has been fulfilled or not.
Bare-bones
Let's write a basic HTTP server program using Node.js.
First things first, import the built-in http
module. The ES6 syntax to do so is:
import http from 'http'
const port = 8080;
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello there');
}).listen(port, () =>
console.log('Server is running at', port)
);
Comments
This server program "listens" for HTTP requests at port 8080.
Whenever a request is made, the callback within the
createServer
method is invoked.The parameters,
req
andres
, are built-in objects. They have methods and properties to analyze a request and send a response accordingly.res.writeHead
sends the status code (200) along with the "Content-Type" specification to the client. This metadata helps the client interpret the response data correctly; in our case, the response is displayed as plain text by a browser.res.end
is the function that actually sends the response data, which in our case is the string 'Hello there'.
Client
A client, such as a browser, uses a URI (Uniform Resource Identifier) to request specific data from a server. An example URI is shown below:
N.B.
req.url
property holds the query string = pathname + query param.To split the query string into more readable parts, we use the
url
module:url.parse(req.url, true)
returns a parsed url object.
How to be RESTful?
An API is basically a set of rules that dictate how different programs interact with each other, whereas REST is an architectural style for building http-based services. So, a RESTful API is an API that follows the principles and constraints enunciated by REST.
๐ Key features of RESTful APIs :
Resources are exposed through distinct paths (routes or endpoints). Every resource has a unique URL.
Standard HTTP methods, such as GET and POST, are leveraged to operate on these resources.
Stateless - Once a request-response cycle completes, the server has no idea of the client. If the client initiates another cycle, both the machines need to share their info to each other anew.
Resources are represented in standard formats like JSON, so that data can be easily exchanged throughout the web.
BU!LD
We need 3 things, namely, middleware functions, a router, and a router handler.
Router and Middlewares
Middlewares are functions with access to the req
and res
objects, allowing them to handle a request-response cycle.
Now, let's look at a simple router.
In the above router
object, the keys are based on pathname
. The purpose of this data structure is to specify which middleware function will be called for a particular pathname
and request method
. Simply put, it "routes" (directs) the client-request to be processed by the correct middleware.
That being said, let's write a couple of middlewares. For simplicity, I'm focusing on two methods only, but you can have as many routes and methods as you need.
GET
Suppose you have some JSON data in your file system that needs to be delivered as a response whenever a client hits the /user/data
endpoint using the GET method. The corresponding middleware is shown below:
At first, import { readFile } from 'fs/promises'
โ ๏ธ
POST
Say we want our API to allow a client to send some JSON payload to be added to the data in our file system only if it hits the endpoint /user/register
with the POST method. The corresponding middleware is shown below:
At first, ensure you have import { readFile, writeFile } from 'fs/promises'
.
N.B. In both cases, any server-side error is handled by the catch
block, and an error message is sent to the client with status code 500.
Finishing touch
The last piece of the puzzle is a component that analyzes a client's request, looks through the router object, and, if possible, triggers the appropriate middleware; otherwise, a 'Not Found' message is sent to the client.
That component is called the "Router Handler".
The if-block in the above code is the most crucial element because it checks whether the pathname and the request method exist in the router object. If yes, the corresponding middleware is invoked; otherwise, a 404 status code is sent with a JSON error message.
Use the above code block as the body of the callback within the http.createServer()
method and...
WE DID IT! ๐ฅณ
Grateful for your time!
See ya soon ๐