HTTP Basics
HTTP or hypertext transfer protocol allows transferring data across the web. It is a stateless request & response text-based protocol using a client-server communications model.
- Request Types, Methods or Verbs
- GET – Retrieve a resource
- POST – Create a resource
- PUT – Update a resource/ (generally) replace a resource with a new resource
- PATCH – Update a resource/ (generally) allow partial resource body update
- DELETE – Delete a resource
- HEAD – The same as GET but only return headers and not content
- OPTIONS – Get the options for the resource
- TRACE – Performs message loop-back
- Optional Headers
- Headers are used to convey additional information between the client and the server
- They consist of a header name + colon + header value, e.g., connection: keep-alive
- Common headers – Request, Response, Connection, Host, User-Agent, Accept Request
- Status Codes
- Info
- 1xx, e.g., 100 – Continue, 101 – Switching Protocols, 102 – Processing
- Success
- 2xx, e.g., 200 – OK, 201 – Created, 202 Accepted, 204 – No Content
- Multiple Choice
- 3xx, e.g., 300 – Multiple Choices, 302 – Found, 307 – Temporary Redirect
- Client Error
- 4xx, e.g., 401 – Unauthorized, 403 – Forbidden, 404 – Not Found, 411 – Length Required
- Server Error
- 5xx, e.g., 500-Internal Server Error, 501 – Not Implemented, 502 – Bad Gateway
- Info
REST, API, JSON and XHR
- REST
- Representational (RE) State(S) Transfer (T)
- It is a software architectural style that was created to guide the design and development of the architecture for the World Wide Web.
- API
- Application (A) Programming (P) Interface (I)
- It is a contract between an information provider and an information user—establishing the content required from the consumer (the call) and the content required by the producer (the response).
- JSON
- JavaScript (JS) Object (O) Notation (N)
- XHR
- XML (X) HTTP (H) Request (R) – a JavaScript API to create AJAX requests. Its methods provide the ability to send network requests between the browser and a server.
Key Constraints for REST
- Client-Server
- Separation of concerns – clients, servers, and resources, with requests managed through HTTP.
- Stateless
- No client information is stored between get requests, and each request is separate and unconnected.
- Cacheable
- Cacheable data that streamlines client-server interactions.
- Layered
- The ability to leverage load balancers (LB), proxies (PXY), and firewalls (FW).
- Code on demand
- The ability to send executable code to the client when requested, extending client functionality.
- Uniform interfaces
- Resource identification in requests
- Resource manipulation through representations
- Self-descriptive messages
- Hypermedia as the engine of application state (HATEOAS)
Node.js & Express
Node.js is an open-source, cross-platform, back-end JavaScript runtime environment that runs on the V8 engine (Google Chrome) and executes JavaScript outside a web browser. A node app runs in a single process, on a single thread. Node.js provides a set of asynchronous I/O primitives in its standard library that prevent JavaScript code from blocking. Generally, libraries in Node.js are written using non-blocking paradigms, making blocking behaviour the exception rather than the norm. Express is a fast, unopinionated, minimalist web framework.
Sample (CRUD) API with Express
Create (C) Read (R) Update (U) Delete (D) API based on a JSON file acting as a persistence layer, with simple routing and cross-origin resource sharing support.
mkdir my_api
cd my_api
mkdir html # static content
mkdir repos # file access module
mkdir assets # sample json data
npm init
npm install express --save
npm install nodemon # monitor and restart node on code change
npm install cors # enable cors support
npm install --save helmet # disable X-Powered-By header
Update Package.json config for a more effortless development experience.
"start": "nodemon index.js",
index.js implementation using express and custom data repository.
const express = require('express');
const helmet = require('helmet');
let app = express();
let router = express.Router();
let cors = require('cors');
let dataRepo = require('./repos/dataRepo');
app.use(express.json());
app.use(cors());
app.use(helmet());
router.get('/', function(req, res, next){
dataRepo.get(function(data){
res.status(200).json({
"status": 200,
"statusTest": "OK",
"data": data
});
},function(err) {
next(err);
});
});
router.get('/search', function(req, res, next){
let searchObject = {
"id" : req.query.id,
"name" : req.query.name
};
dataRepo.search(searchObject, function(data){
res.status(200).json({
"status" : 200,
"statusText" : "OK",
"data": data
});
},function(err){
next(err);
});
});
router.get('/:id', function(req, res, next){
dataRepo.getById(req.params.id, function(data){
if(data){
res.status(200).json({
"status": 200,
"statusTest": "OK",
"data": data
});
}
else {
res.status(404).json({
"status": 404,
"statusTest": "Not Found",
"message": "Item " + req.params.id + " not found",
"error": {
"code" : "NOT_FOUND"
}
});
}
}, function(err){
next(err);
}
);
});
router.post('/', function(req, res, next){
dataRepo.insert(req.body, function(data){
res.status(201).json({
"status": 201,
"statusText": "Created",
"data": data
});
}, function(err){
if(err){
next(err);
}
});
});
router.put('/:id', function(req, res, next){
dataRepo.getById(req.params.id, function(data){
if(data){
fruitRepo.update(req.body, req.params.id, function(data){
res.status(200).json({
"status":200,
"statusTest": "OK",
"data" : data
});
});
} else{
res.status(404).json({
"status": 404,
"statusText": "Not Found",
"message" : "Item " + req.params.id + " not found",
"error": {
"code": "NOT_FOUND",
"message" : "Item " + req.params.id + " not found",
}
});
}
});
});
router.delete('/:id', function(req, res, next){
dataRepo.getById(req.params.id, function(data){
if(data){
fruitRepo.delete(req.params.id, function(data){
res.status(200).json({
"status":200,
"statusTest": "OK",
"data" : "Item " + req.params.id + " deleted"
})
});
} else{
res.status(404).json({
"status": 404,
"statusText": "Not Found",
"message" : "Item " + req.params.id + " not found",
"error": {
"code": "NOT_FOUND",
"message" : "Item " + req.params.id + " not found",
}
});
}
});
});
router.patch('/:id', function (req, res, next) {
dataRepo.getById(req.params.id, function (data) {
if (data) {
itemRepo.update(req.body, req.params.id, function (data) {
res.status(200).json({
"status": 200,
"statusText": "OK",
"message": "Item '" + req.params.id + "' patched.",
"data": data
});
});
}
else {
res.status(404).send({
"status": 404,
"statusText": "Not Found",
"message": "Item '" + req.params.id + "' could not be found.",
"error": {
"code": "NOT_FOUND",
"message": "Item '" + req.params.id + "' could not be found."
}
});
}
}, function (err) {
next(err);
});
});
app.use('/api/', router);
var server = app.listen(5000, function(){
console.log('Node running on http://localhost:5000');
});
Example data repository (dataRepo) implementation using Node’s file system module.
let fs = require('fs');
const FILE_NAME = './assets/data.json';
let dataRepo = {
get: function(resolve, reject) {
fs.readFile(FILE_NAME, function(err, data){
if(err){
reject(err);
} else{
resolve(JSON.parse(data))
}
});
},
getById: function(id, resolve, reject) {
fs.readFile(FILE_NAME, function(err, data){
if(err){
reject(err);
} else{
item = JSON.parse(data).find(i=> i.id == id);
resolve(item);
}
});
},
search: function(searchObject, resolve, reject){
fs.readFile(FILE_NAME, function (err, data){
if(err){
reject(err);
}else{
let items = JSON.parse(data);
if(searchObject){
results = items.filter(
i=> (searchObject.id ? i.id == searchObject.id : true) &&
(searchObject.name ? i.name.toLowerCase().indexOf(searchObject.name.toLowerCase()) >= 0 : true))
resolve(results);
}
}
});
},
insert: function(newData, resolve, reject){
fs.readFile(FILE_NAME, function(err, data){
if(err){
reject(err);
}else{
let items = JSON.parse(data);
items.push(newData);
fs.writeFile(FILE_NAME, JSON.stringify(items), function(err){
if(err){
reject(err);
} else{
resolve(newData);
}
});
}
});
},
update: function(newData, id, resolve, reject){
fs.readFile(FILE_NAME, function(err, data){
if(err){
reject(err);
}else{
let items = JSON.parse(data);
let item = items.find(i=> i.id == id);
if(item){
Object.assign(item, newData);
fs.writeFile(FILE_NAME, JSON.stringify(items), function(err){
if(err){
reject(err);
}else{
resolve(newData);
}
});
}
}
});
},
delete: function(id, resolve, reject){
fs.readFile(FILE_NAME, function(err, data){
if(err){
reject(err);
}else{
let items = JSON.parse(data);
let index = items.findIndex(i=>i.id == id);
if(index != -1){
items.slice(index, 1);
fs.writeFile(FILE_NAME, JSON.stringify(items), function(err, index){
if(err){
reject(err);
}else{
resolve(index);
}
});
}
}
});
}
};
module.exports = dataRepo;
Consumer HTML using XHR object and the API.
<!DOCTYPE html>
<html>
<head>
<title>Sample</title>
</head>
<body>
<h1>Sample</h1>
<button onclick="getAllItems();">Get All Items</button>
<script>
'use strict';
const URL = "http://localhost:5000/api/";
function getAllItems() {
console.log("GET All");
let req = new XMLHttpRequest();
req.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
let response = JSON.parse(this.response);
console.log(response.status);
console.log(response.statusText);
console.log(response.data);
}
};
req.open("GET", URL);
req.send();
}
</script>
</body>
</html>
To run the API locally on http://localhost:5000/, execute the following:
npm start # run the 'start' script as per config in 'package.json
# OR
node start # default, execute the 'server.js' file in 'node' when no config
Then, open index.html to test the CORS example, then test the API in Postman.