Your first API (Node.js + Express)

Gus
Frontend at Accenture
8 min read4 days ago
Photo by israel palacio on Unsplash

As front-end developers, we often concentrate on the user interface and the user experience of our applications. However, have you ever considered the origin and storage of the data we display or save? Where does it come from? And where is it stored?

As I advanced in my development career, I realized that focusing solely on the front end was not enough. I needed to expand my knowledge to include various technologies, particularly backend development. This is where Node.js came into the picture. I preferred Node.js because it uses JavaScript, a language I was already familiar with.

To begin backend development, it’s essential to first understand some basic concepts: RESTful APIs, HTTP verbs, and HTTP status codes. Let’s deep dive into them.

RESTful APIs

What is an API? In simple words, it’s how two applications communicate with each other. The first application, often called the “client” (typically a front-end application), makes a request through the API. The second application, known as the “server” (usually a back-end application), sends back a response with the requested information.

Today, most APIs are RESTful APIs. This means that they follow certain rules and constrains known as Representational State Transfer (REST), an architectural style for designing networked applications. RESTful APIs use HTTP requests to perform CRUD (Create, Read, Update, Delete) operations on resources identified by URIs.

You might be curious: what exactly is a URI? URI stands for Uniform Resource Identifier, which is a string used to uniquely identify a specific resource, allowing efficient access to it on the server.

HTTP Verbs

When the client sends an HTTP request, it employs HTTP verbs to perform CRUD operations. The most commonly used verbs are:

  • GET to read information about a resource.
  • POST to create a resource.
  • PATCH or PUT to update a resource information.
  • DELETE to remove a resource.

We will explore these in more detail with code examples later in this article.

HTTP Status Codes

Once the server processes the client request, typically by establishing connections to a database to fetch data, it responds with an HTTP status code (a three-digit number), along with headers and a body containing the requested data.

HTTP status codes can be broadly categorized into three groups:

  • Codes starting with 2xx indicate success.
  • Codes starting with 4xx indicate client-side errors, accompanied by a message advising what needs correction
  • Codes starting with 5xx indicate server-side errors, where no action can be taken by the client.

Now that we’ve had a brief overview of how an API works, let’s dive into the code.

To get started with Node.js, the first step is to install it. You can download the package from the official website, or use a package manager. Personally, I use nvm (Node Version Manager), which allows me to work on different projects with different Node.js versions without the need to uninstall and reinstall the Node package directly.

Once you have your Node.js environment set up, the next step is to create a directory to hold all your API content (our Express application). Make this directory your working directory.

mkdir myFirstApi
cd myFirstApi

Use the npm init command to create a package.json file for your application. This command will prompt you with some questions about your application. You can accept the default values or customize them. Typically, I use the default values except for the entry point file, which I change from index.js to main.js or app.js.

Now, install Express in your directory and save it to the dependencies list by running the following command:

npm install express

We are now ready to start creating our server and endpoints. The server initialization will take place in the main file.

// -- app.js

// Import the package to generate an instance
const express = require("express");

// Declare the host and port where our API is going to be running
const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ? Number(process.env.PORT) : 3000;

// Generate an instance of express - this will be our application
const app = express();

// Since express is not only to generate REST APIs
// the body of the request is not in JSON format by default.
// With this middleware we can read the body as a JSON Object
app.use(express.json());

// Endpoints
app.get('/', (req, res) => {
res.send({ message: 'Hello First API!' });
});

// Use method listen to start your application
app.listen(port, host, () => {
console.log(`[ ready ] http://${host}:${port}`);
});

With the previous setup, you can run your server using the following command:

node app.js

You should now be able to see your server running at http://localhost:3000.

At the end, our application will have the following structure:

.
├── app.js
├── package.json
├── controllers
│ └── user.js
└── routes
└── users.js

Now that our application is running, we can add custom endpoints. First, we’ll create a controller to handle the logic. In this simple case, we’ll use an array as our database. Normally, the controller connects with the database to get or set the necessary information.

We’ll create user.js inside the controllers folder and add the logic to handle the CRUD operations for users.

// -- controllers/user.js

// Initialize users array to save data in memory
const users = [];

// Get all users
const getUsers = (req, res) => {
// Return all the users with a 200 status code
res.status(200).json(users);
};

// Get user by id
const getUserById = (req, res) => {
// Retrieve the id from the route params
const { id } = req.params;
// Check if we have a user with that id
const user = users.find((u) => u.id === id);

if (!user) {
// If we don't find the user return a 404 status code with a message
return res.status(404).json({ message: 'User not found' });
// Note: Remember that json method doesn't interrupt the workflow
// therefore is important to add a "return" to break the process
}

// Return the user with a 200 status code
res.status(200).json(user);
};

// Create user
const createUser = (req, res) => {
// Retrieve the name from the request body
const { name } = req.body;

if (!name) {
// If name is empty or undefined return a 400 status code with a message
return res.status(400).json({ message: 'The name is required.' });
}

// Generate a new user
const newUser = {
id: Date.now().toString(), // Convert id to string to match the value in get by id endpoint
name
};
// Add the new user to our array
users.push(newUser);

// Return the created user with a 201 status code
res.status(201).json(newUser);
};

// Update user
const updateUser = (req, res) => {
// Retrieve the id from the route params
const { id } = req.params;
// Retrieve the index of the user in the array
const userIndex = users.findIndex((u) => u.id === id);

// "findIndex" will return -1 if there is no match
if (userIndex === -1) {
// If we don't find the user return a 404 status code with a message
return res.status(404).json({ message: 'User not found' });
}

// Generate a copy of our user
const updatedUser = { ...users[userIndex] };
// Retrieve the name from the request body
const { name } = req.body;

// Check if we have a name, if so update the property
if (name) {
updatedUser.name = name;
}

// Update the user in our array
users[userIndex] = updatedUser;

// Return the updated user with a 200 status code
res.status(200).json(updatedUser);
};

// Delete user
const deleteUser = (req, res) => {
// Retrieve the id from the route params
const { id } = req.params;
// Retrieve the index of the user in the array
const userIndex = users.findIndex((u) => u.id === id);

// "findIndex" will return -1 if there is no match
if (userIndex === -1) {
// If we don't find the user return a 404 status code with a message
return res.status(404).json({ message: 'User not found' });
}

// Remove the user from the array
users.splice(userIndex, 1);

// Return a 204 status code
res.status(204).send();
};

// Export the methods to be used in the route
export default {
getUsers,
getUserById,
createUser,
updateUser,
deleteUser
};

Now that we have the logic to handle all the user data, we can proceed to generate the routes from which this data will be accessible.

We’ll create users.js inside the routes folder to connect our logic with the endpoints.

// -- routes/users.js

const express = require("express");

// Import the controller created
const userController = require('../controllers/user');

// Generate a router from express instance to add CRUD operations
const router = express.Router();

// Get all users - Generate a GET method
router.get('/', userController.getUsers);

// Get user by id - Generate a GET method with a parameter
// In the controller we can obtain the value of this parameter
// with "const { id } = req.params;"
router.get('/:id', userController.getUserById);

// Create user - Generate a POST method
router.post('/', userController.createUser);

// Update user - Generate a PATCH method with a parameter
router.patch('/:id', userController.updateUser);

// Delete user - Generate a DELETE method with a parameter
router.delete('/:id', userController.deleteUser);

// Export the router so we can use it in our app.js
module.exports = router;

What we are doing here is generating all the routes (endpoints) for the user resource. With our routes connected to our logic, the only remaining step is to add this to our app instance in the main file (app.js).

// -- app.js

// Import the router
const users = require('./routes/users');

// ...

// Define the prefix of the endpoints and link it to the users router
app.use('/api/users', users);

// ...

You can now start your server again and try your new endpoints. These endpoints will be accessible through http://localhost:3000/api/users.

Conclusion

You can create a very basic API with just a few files. While this example isn’t production-ready and has plenty of room for improvement, it’s a solid first step into backend development.

Next steps for this API

  • Add Security Packages: Implement security measures by using packages like helmet to help secure your app by setting various HTTP headers.
  • Implement CORS: Add Cross-Origin Resource Sharing (CORS) to control which resources can be accessed by external domains, enhancing your app’s security.
  • Connect to a Database: Instead of saving data in memory, connect to a database. MongoDB with Mongoose is a popular choice with extensive documentation.
  • Add Authentication: Implement JSON Web Token (JWT) authentication to secure your endpoints and manage user sessions.
  • Explore Middlewares: Play around with middlewares to handle various tasks such as logging, authentication, and validation.

I hope this helps you understand how an API works and how you can start building your own. Thank you for your time!

--

--