Content
- Introduction
- Project creation and initialization
- Create a simple server and a GET route
- Routes and Request handlers
- Request and Response
- Watch for changes
- Create POST, GET, UPDATE, and DELETE routes
- API Clients
- Request body, params, query, header, ...
- Manipulating memory data
- Conclusion
Introduction
In .
Now we have to create the parent folder for our API by doing:
# create a folder for the project at the path of your choice
mkdir expense-tracker-simple-api
# open the project with vscode
# code expense-tracker-simple-api
code -r expense-tracker-simple-api
# open the built-in terminal and init node
npm init -y
# this should create the package.json file
# create the entry file, index.js
echo "console.log(\"expense-tracker-simple-api\");" > index.js
# run the index.js file
node index.js
All we are doing with this script is quite direct.
- We create a folder for our project
- We opened the folder in vscode
- We initialized a nodejs project
- We added a console log into the index.js file. This creates the file and adds some content
- We execute the index.js file
An alternative is to go to wherever you want to create this folder and create it over there then open the folder in vscode and init node project - check out, . Express is a library that will help us create our APIs.
We can install this package by running, npm i express. This should modify the package.json file, and create the package-lock.json file and node_modules folder. Consult the excerpt, .
app.listen(3000, () =>
console.log(`Api running on ${"http://localhost:3000"}`)
);
We used the express application to listen on a port and used an arrow function to tell us, tell developers, that our application is running. For the port, we can alter it to another port of our choice. However, some special ports are already meant or used for some particular task and they are well known in the community and as such servers as default when such applications or programs are running on our PC. Check these out - in the browser. What do you see? A text saying, Hello world ?
Watch for changes
Rome was not built in one day as the saying goes. The same applies to software development. Maybe here what we mean is that we will gradually add more features as we develop and in this continuous process, it becomes irritating to start and stop the server all the time.
Go on, add another GET request (route) with /hello path and a request handler that says something you would want to say. Be happy.
You'd have to restart the server (the running nodejs process) and visit, , is an endpoint. You share this with api consumers. Among ourselves, we say route, because we don't have to know the whole URL (including protocol - http, domain - localhost, port - 3000, and path - /hello). Route is METHOD + PATH, more or less, GET /hello.
On macOS or Windows, we can do node --watch index.js or we can look out for changes not just in our entry file but in the whole folder path by, node --watch-path=./ index.js to watch for changes in the file path and also the file itself.
Currently, this is the content of my package.json file:
{
"name": "expense-tracker-simple-api",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.2"
}
}
We can add a script called dev under the script section.
"dev" : "node --watch-path=./ index.js"
We can stop the running server with control + c and now execute npm run dev. This will monitor saved changes in our files and reload the server.
So if this is not working for you, we have an alternative. We will install nodemone, npm i nodemon -g. We'd use it everywhere as a utility tool so we don't have to install it as part of our packages. We can watch changes by executing, nodemon index.js. There are cases where this won't work and when it doesn't, dom nodemon --exec node index.js
We can modify our dev script to use nodemon by,
"dev" : "nodemon --exec node index.js"
At this point, you can freely modify your .js files and on save, the server will restart to reload the load changes applied.
Create POST, GET, UPDATE, DELETE routes
We have already created a GET request. In this section, we will look into what each method means briefly since we have discussed them at length in , , etc...
In vscode and alongside some other IDEs, there is an extension that provides extensions to api clients. vscode has some of these extensions for that matter. However, we will be considering an api client known as .
.http or .rest extension - touch expense-tracker-api.httpexpense-tracker-api.http we can define our requestGET request, add the following to the .http file GET http://localhost:3000
- The endpoint is passed as seen above. For a post, put or delete a request to update the endpoint. Remember the difference between an endpoint and a route?
- For a request that requires that data be passed to the api, we can pass the data as part of the route as a parameter or a string query, or we can pass it in the body.
POST http://localhost:3000
Content-Type: application/json
{
"property1": "value1",
"property2": "value1",
...
"propertyN": "valueN",
}
Content-Type: application/jsonis a header key-value. This means that's how you pass headers using rest-client.- For the request body, we pass it as a json object - a newline is expected between the headers and the body though
- Each request can be separated by three pound or ash signs,
###. A text can be added at the end of###to make it look as if it is a title.
### test base GET endpoint
GET http://localhost:3000
### Create a dummy POST request
POST http://localhost:3000
Content-Type: application/json
{
"property1": "value1",
"property2": "value1",
...
"propertyN": "valueN",
}
As an exercise, create the request for the expenditure endpoints. Refer to to communicate with our api using the API client.
As mentioned earlier, we can pass data to our api using the body, header, or URL of our request. We have seen how to pass data via the request body and the header (We will look into passing some specific data at another time). Check the POST request created. What we have not looked at is how to pass data as part of the URL.
Let's say we want to read an expenditure which has an id of 4, we can pass the add a parameter (as part of the URL) as, /expenditures/2. For the request which will be handling this requirement, we do, /expenditures/:id, where :id refers to the id of the expenditure. Assuming it is something else other than an id, let's say a name, then we'd do :name. Express will process this a provide us with a means to extract this value without sweat.
Now, for a query string, the idea is similar to request parameters however, it comes after a question, followed by a key1=value1&key2=value2...&keyN=valueN, where the key is the identifier of the value you want to pass. A very direct example is the REST-Client URL, , glance through.
We can alter the status code by passing the desired status code in res.status(desireStatusCode).json(obj). I will maintain the 200 status code throughout.
Make sure the server is still running
We can pass the list of expenditures directly.
// list expenditures
app.get("/expenditures", (req, res) => {
return res.json(expenditures);
});
What was the response received? Check the status code as well as the response payload.
From experience and also to avoid ambiguity, I prefer to return status code 200 by default and have a either success property, message or data property to return a message or requested resource. By default, when status is false, message will be passed else, message or data may be passed.
// list expenditures
app.get("/expenditures", (req, res) => {
return res.json({
success: true,
data: expenditures,
});
});
We need to display the id (index of each row)
// list expenditures
app.get("/expenditures", (req, res) => {
return res.json({
success: true,
data: expenditures.map((row, index) => ({ id: index, ...row })),
});
});
Apply filtering with
// list expenditures
app.get("/expenditures", (req, res) => {
// get the query string and check if it is not a number or something
// that can be a number else set a default filter value of 0
let amountMoreThan = Number(req.query.amountMoreThan);
if (isNaN(amountMoreThan) || amountMoreThan < 0) {
amountMoreThan = 0;
}
return res.json({
success: true,
data: expenditures
.map((row, index) => ({ id: index, ...row }))
.filter((row) => row.amount > amountMoreThan),
});
});
Why was the filter done after the mapping?
Read expenditure
// read expenditure
app.get("/expenditures/:id", (req, res) => {
// get the id of the request resource from the params
const id = Number(req.params.id);
if (isNaN(id) || id < 0) {
return res.status(200).json({
success: false,
message: "Resource ID invalid",
});
}
// we don't take negative ids, why?
const row = expenditures[id];
if (!row) {
// what status code do this should be passed here? 404 not not found? why??
return res.status(200).json({
success: false,
message: `Resource with ID, '${id}' not found`,
});
}
return res.status(200).json({
success: true,
data: row,
});
});
Does this implementation hint to you about, Why was the filter done after the mapping? ?
Create expenditure
// create expenditure
app.post("/expenditures", (req, res) => {
// const { name, amount, date } = req.body
const payload = req.body;
if (!payload?.name || !payload.amount || !payload.date) {
return res.status(200).json({
success: false,
message: "name, amount and date for expense are required",
});
}
// we have to validate the name, maybe, their name must have some number of characters
// we have to make sure that amount is a number
// we have to also make sure that the date is of the format, yyyy-MM-dd
// insert the new record
expenditures.push({
name: payload.name,
amount: payload.amount,
date: payload.date,
});
return res.status(201).json({
success: true,
message: "expenditure created successfully",
});
// there situations that it is best that you pass the new record created
/* return res.status(201).json({
success: true,
message: "expenditure created successfully",
data: expenditures[expenditures.length - 1],
}); */
// pass a route to fetch the new record created
/* return res.status(201).json({
success: true,
message: "expenditure created successfully",
route: `/expenditures/${expenditures[expenditures.length - 1]}`,
}); */
});
Update expenditure
// update expenditure
app.put("/expenditures/:id", (req, res) => {
// get the id of the request resource from the params
const id = Number(req.params.id);
if (isNaN(id) || id < 0) {
return res.status(200).json({
success: false,
message: "Resource ID invalid",
});
}
// we don't take negative ids, why?
const row = expenditures[id];
if (!row) {
// what status code do this should be passed here? 404 not not found? why??
return res.status(200).json({
success: false,
message: `Resource with ID, '${id}' not found`,
});
}
// we have to validate the name, maybe, their name must have some number of characters
// we have to make sure that amount is a number
// we have to also make sure that the date is of the format, yyyy-MM-dd
const { name, amount, date } = req.body;
expenditures[id].name = name ?? row.name;
expenditures[id].amount = amount ?? row.amount;
expenditures[id].date = date ?? row.date;
// there was a time I saw a 204 status - No content
// since there was no data to return however we'll return the usual
return res.status(200).json({
success: true,
message: "expenditure updated successfully",
});
});
Delete expenditure
// delete expenditure
app.delete("/expenditures/:id", (req, res) => {
// get the id of the request resource from the params
const id = Number(req.params.id);
if (isNaN(id) || id < 0) {
return res.status(200).json({
success: false,
message: "Resource ID invalid",
});
}
// we don't take negative ids, why?
const row = expenditures[id];
if (!row) {
// what status code do this should be passed here? 404 not not found? why??
return res.status(200).json({
success: false,
message: `Resource with ID, '${id}' not found`,
});
}
expenditures = expenditures.filter((_, index) => index !== id);
return res.status(200).json({
success: true,
message: "expenditure deleted successfully",
});
});
Conclusion
We have covered the root of most API developments. This project is as basic as it comes. Relax and glance through again. There is more to look into such as
- validation
- authentication and authorization
- middleware
- error handling
- SQL
- database integration
Practice project
crud api = create, list, read, update, and delete. It is how you approach these problems.
To-Do List
- todo object: { id: int, task: string, status: boolean }
- crud api
- add an endpoint to mark all tasks as completed, success is true or not completed
Calculator
- you have to decide, whether you'd create an endpoint for all the operations (addition, subtraction, multiplication, division)
- or you would create a single endpoint with different functions that correspond to each operation. The user should be able to pass the operator and the two operands
Currency Converter
You are converting from one currency to another. Do for as many currencies as you can (3 is enough)
- Unit Converter
- Notes App
- Personal Blog
- Quiz App
Snippets
Know that the excess was removed.
// import the express lib
const express = require("express");
// dummy data
let expenditures = [
{
name: "Legion Tower 7i Gen 8 (Intel) Gaming Desktop",
amount: 2099.99,
date: "2024-12-31",
},
{
name: "Apple MacBook Pro 16-inch",
amount: 2499.99,
date: "2024-12-15",
},
{
name: "Samsung Galaxy S24 Ultra",
amount: 1199.99,
date: "2024-12-10",
},
];
// create an express application
const app = express();
// parse request body as json
app.use(express.json());
// list expenditures
app.get("/expenditures", (req, res) => {
// get the query string and check if it is not a number or something
// that can be a number else set a default filter value of 0
let amountMoreThan = Number(req.query.amountMoreThan);
if (isNaN(amountMoreThan) || amountMoreThan < 0) {
amountMoreThan = 0;
}
return res.json({
success: true,
data: expenditures
.map((row, index) => ({ id: index, ...row }))
.filter((row) => row.amount > amountMoreThan),
});
});
// read expenditure
app.get("/expenditures/:id", (req, res) => {
// get the id of the request resource from the params
const id = Number(req.params.id);
if (isNaN(id) || id < 0) {
return res.status(200).json({
success: false,
message: "Resource ID invalid",
});
}
// we don't take negative ids, why?
const row = expenditures[id];
if (!row) {
// what status code do this should be passed here? 404 not not found? why??
return res.status(200).json({
success: false,
message: `Resource with ID, '${id}' not found`,
});
}
return res.status(200).json({
success: true,
data: row,
});
});
// create expenditure
app.post("/expenditures", (req, res) => {
// const { name, amount, date } = req.body
const payload = req.body;
if (!payload?.name || !payload.amount || !payload.date) {
return res.status(200).json({
success: false,
message: "name, amount and date for expense are required",
});
}
// we have to validate the name, maybe, their name must have some number of characters
// we have to make sure that amount is a number
// we have to also make sure that the date is of the format, yyyy-MM-dd
// insert the new record
expenditures.push({
name: payload.name,
amount: payload.amount,
date: payload.date,
});
return res.status(201).json({
success: true,
message: "expenditure created successfully",
});
// there situations that it is best that you pass the new record created
/* return res.status(201).json({
success: true,
message: "expenditure created successfully",
data: expenditures[expenditures.length - 1],
}); */
// pass a route to fetch the new record created
/* return res.status(201).json({
success: true,
message: "expenditure created successfully",
route: `/expenditures/${expenditures[expenditures.length - 1]}`,
}); */
});
// update expenditure
app.put("/expenditures/:id", (req, res) => {
// get the id of the request resource from the params
const id = Number(req.params.id);
if (isNaN(id) || id < 0) {
return res.status(200).json({
success: false,
message: "Resource ID invalid",
});
}
// we don't take negative ids, why?
const row = expenditures[id];
if (!row) {
// what status code do this should be passed here? 404 not not found? why??
return res.status(200).json({
success: false,
message: `Resource with ID, '${id}' not found`,
});
}
// we have to validate the name, maybe, their name must have some number of characters
// we have to make sure that amount is a number
// we have to also make sure that the date is of the format, yyyy-MM-dd
const { name, amount, date } = req.body;
expenditures[id].name = name ?? row.name;
expenditures[id].amount = amount ?? row.amount;
expenditures[id].date = date ?? row.date;
// there was a time I saw a 204 status - No content
// since there was no data to return however we'll return the usual
return res.status(200).json({
success: true,
message: "expenditure updated successfully",
});
});
// delete expenditure
app.delete("/expenditures/:id", (req, res) => {
// get the id of the request resource from the params
const id = Number(req.params.id);
if (isNaN(id) || id < 0) {
return res.status(200).json({
success: false,
message: "Resource ID invalid",
});
}
// we don't take negative ids, why?
const row = expenditures[id];
if (!row) {
// what status code do this should be passed here? 404 not not found? why??
return res.status(200).json({
success: false,
message: `Resource with ID, '${id}' not found`,
});
}
expenditures = expenditures.filter((_, index) => index !== id);
return res.status(200).json({
success: true,
message: "expenditure deleted successfully",
});
});
// create a server that listens to requests on port 3000
app.listen(3000, () =>
console.log(`Api running on ${"http://localhost:3000"}`)
);
API requests
# Expenditure endpoints
### Create Expenditures
POST http://localhost:3000/expenditures
Content-Type: application/json
{
"name": "Legion Tower 7i Gen 8 (Intel) Gaming Desktop",
"amount": 2099.99,
"date": "2024-31-12"
}
### List Expenditures
GET http://localhost:3000/expenditures?
### Read Expenditure
GET http://localhost:3000/expenditures/1
### Update Expenditure
PUT http://localhost:3000/expenditures/11
Content-Type: application/json
{
"name": "MacBook pro laptop",
"amount": 5099.99,
"date": "2025-01-01"
}
### Delete Expenditure
DELETE http://localhost:3000/expenditures/0
SOCIAL SHARE CARD GENERATOR