DEV Community

Cover image for How Every Web Developer Can Become FullStack With Node.js
dastasoft
dastasoft

Posted on • Originally published at blog.dastasoft.com

How Every Web Developer Can Become FullStack With Node.js

I'm sure you've heard of Node.js but maybe you haven't delved into it or you only have a general idea of what it is and what it's for. I want to explain what Node is and why you should use it, especially if you are in web development and want to expand your tool belt or your job opportunities. We are also going to see why to use some libraries and frameworks that are built on top of Node to make our life easier and our code cleaner.

Through this guide we will see what Node and Express is and how it works, build a REST API to store and retrieve data, test endpoints and upload our application.

By the end of this series you will have a complete overview of the MERN stack (MongoDB, Express, React and Node) and testing skills.

Roadmap

I want to give you also a roadmap of this series, the idea is that starting from a basic knowledge of Node and Express, we will see how to store and retrieve data from the server but for now using only the file system. In future guides we will see how to transform this into a real database data retrieval/storage and even how to deploy to the cloud.

In this series we will also create a React application that will use this back-end we are creating now. If you use or have just started using Next.js you may have noticed that Next.js comes with a Node "inside", the api.js. I think it is important to experiment with flat Node before you first encounter it inside Next.js but we will see how much of the code we are building today is reused in a project built with Next.js too.

TypeScript

In the sample project I will be using TypeScript instead of plain JavaScript, you can follow it without worries because the syntax is quite similar but if you wonder why you should bother dealing with TS instead of JS I recommend you read my last post.

My last post was for TypeScript on the front-end but everything explained there is applicable here. If in the front-end TS is useful in the back-end it is even more useful because back-end development usually has more logic and let's say more critical than front-end development, but take this statement with a grain of salt.

Resources

Project Sample

In this guide we are going to work on a simple REST API that stores and retrieves data from JSON files stored on the server. This REST API is intended to build a job posting application, where users can enter a company, and different job postings.

What is Node.js?

As you know, we are divided into front-end and back-end, until Node.js was released, if we think of JavaScript it was directly targeted at front-end development.

With Node.js, we can run JavaScript on the server side or even directly on a computer. Well, technically a server is a computer, but you get the point. But JavaScript only runs inside the browser, so how can it now run directly on a computer? Node.js is mainly built in C++, Node inside has Google's V8 engine, this engine converts the JavaScript directly into native machine code.

So basically you write your normal JavaScript, which Node passes to V8 which generates machine code and the computer is able to read that code.

node diagram

But Node is much more than a bridge between your JS and V8, through different modules Node allows us, to give some examples, to communicate with the computer's file system or to set up a server that reacts to requests and serves content from/to a database.

That's great but, I'm a web developer who doesn't intend to write applications for Windows or any other OS, how do you put Node.js on the server and replace my fancy Java Spring Boot + Hibernate dynamised with Lombok annotations?

node responses diagram

You'll send a request to the server, from your React or whatever front-end you have, on the server we have a Node.js running that will listen to the request and make a response back to the client. That response, it can be a file, because we have access to the file system, like a full HTML and image or any other binary data.

It can also communicate with a database, retrieve some data, do some calculations and give us back a beautiful JSON ready to use in our front-end.

Why to use Node.js?

  • It's all JavaScript → Even if you look at this from the perspective of your own or the point of view of a company it is still true, just one language and you can make a complete application, both sides. For you it's interesting, reusing your current skills with a language in another field, but for companies this is a good point too, they can reuse the current expertise of their workers.
  • It's all JavaScript x2 → Because both sides are JavaScript, it's very possible to reuse code between both sides, do you already have a function that validates ID cards? Use exactly the same on the front-end and back-end.
  • Community → There are a lot of utilities, packages and even frameworks built on top of Node.js, you will get a lot of support and there are tons of ready to use tools available.
  • It's highly used → Take a look at this screenshot from State of JS 2020, Express which is built on top of Node.js is in a terrible shape. But yes, the "everyone uses it" argument should be taken very carefully.

state of js 2020

Setup

The easiest way to install Node.js on your system is to go to the official website, especially https://nodejs.org/en/download/current/ where all the platforms and options are listed. You can choose between the Long Term Support or the latest version, choose what you want, for the case of this guide both options are good, personally I am using the current version which is 16.5.0.

For Windows and Mac there is no mystery with the installation so if you are Linux user like me, you will find this resource more useful.

For example for Ubuntu users:

curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install -y nodejs
Enter fullscreen mode Exit fullscreen mode

Installing Node.js also installs npm which stands for Node Package Manager, if you come from web development you are more than used to using it.

To check that everything is OK, run the following commands in your terminal

node --version
npm --version
Enter fullscreen mode Exit fullscreen mode

If you type node in your terminal, you will be able to execute JavaScript code in the same way as you do in a Developer Tools inside the browser. If you want to exit, type .exit or use Ctrl+C.

Open your favourite IDE and create a server.js file (the name is totally up to you), in this JS file you can write your normal JavaScript and run it by typing node server on your terminal.

Congratulations, you are now running JavaScript code outside the browser!

Differences running JS on Front and Back

As we have already seen Node.js allows us to execute JavaScript in the back-end of our project but as that JavaScript is executed outside the browser there are some minor differences.

Global Object

In the front-end our global object is the window object, if you inspect that object you will find a number of utilities and variables such as the fancy window.document.getElementById. In Node.js the window object is replaced by the global object.

Use your server.js file created earlier to make console.log(global) and check what's inside. You'll find some familiar functions like setTimeout or setInterval.

console.log(global);

/* <ref *1> Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  performance: [Getter/Setter],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  }
} */
Enter fullscreen mode Exit fullscreen mode

If you look closely, you'll miss a few things, such as the fact that Node doesn't have a document object or any of the other objects related to DOM manipulation.

As in the front-end, you don't need to type global every time you need to access something inside this object, you can use setTimeout directly instead of going to global.setTimeout.

dirname and filename

There are two new utilities available in global that you will use a lot:

  • __dirname will tell you the path to the directory in which the current script is running.
  • __filename returns the name and absolute path of the currently running script.
console.log(__dirname); // /workspace/my-new-project
console.log(__filename); // /workspace/my-new-project/server.js
Enter fullscreen mode Exit fullscreen mode

Splitting Code

If you want to split your code into different files you might be used to import and export from ES6 JavaScript, in Node it's also possible but a lot of the code you'll find on the internet will be with commonJS modules so I think it's important to know that too.

To export members from your current module to others you can use these options:

// module1.js
const name = "dastasoft";
const ultimate = "instant sleep";

module.exports = { name, ultimate };

// module2.js
const animes = ["Death Note", "Jujutsu Kaisen"];

module.exports = animes;

// module3.js
module.exports.today = () => new Date().getDay();
Enter fullscreen mode Exit fullscreen mode

The difference is not only the number of parameters you want to export, but how you use the values:

// module4.js
const { name, ultimate } = require("/module1");
const animes = require("./module2");
const aFunction = require("/module3");

console.log(name); // dastasoft
console.log(ultimate); // instant sleep
console.log(animes); // ["Death Note", "Jujutsu Kaisen"]
console.log(aFunction.today()); // 5
Enter fullscreen mode Exit fullscreen mode

As you can see instead of importing we use require as a keyword to include other modules. The module is just a simple JavaScript variable that is included in all Node modules.

If you try to use ES6 modules you will most likely get the following error:

(node:22784) Warning: To load an ES module, set "type": "module" in 
the package.json or use the .mjs extension.(node:22784) 
Warning: To load an ES module, set "type": "module" in the package.json 
or use the .mjs extension.
Enter fullscreen mode Exit fullscreen mode

There are different ways to solve this:

  • Using the .mjs extension for files you want to use and consume as a module.
  • Setting the type to module in your package.json.
  • Using TypeScript and in the tsconfig.json set the module to commonjs so the TS you write will be transformed into JS using commonjs and Node will be happy with that.

Built-in modules

Along with Node there are some utility modules that you can use without any additional installation, let's see some examples:

OS

The operating system module provides a lot of information about the system it is running on:

const os = require("os");

console.log(os.arch()); // x64
console.log(os.version()); // #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021
console.log(os.platform()); // linux
Enter fullscreen mode Exit fullscreen mode

FS

The filesystem module is one of Node's game changers, you can access the filesystem and perform a lot of actions.

Let's create a filesystem.js to do some testing with the filesystem module:

// filesystem.js
const fs = require("fs");

fs.readFile("./assets/test.txt", (error, data) => {
  if (error) console.log(error);
  else console.log(data.toString());
});
Enter fullscreen mode Exit fullscreen mode

If you do node filesystem you will get the following error message Error: ENOENT: no such file or directory, open './assets/test.txt'.

Create a folder called assets and a test.txt file with some content in it, try again.

Let's add a writeFile function:

// filesystem.js
const fs = require("fs");

fs.readFile("./assets/test.txt", (error, data) => {
  if (error) console.log(error);
  else console.log(data.toString());
});

fs.writeFile("./assets/test.txt", "I'm soooo fast", () => {
  console.log("Done sir");
});
Enter fullscreen mode Exit fullscreen mode

If you try this code, you will see that before you can read the file it is already written with the new text and when readFile does its job it prints the new content. This happens because these two methods are asynchronous and do not block the execution of the code, the code continues to execute line by line and writeFile terminates first.

This is one of the key points of Node.js and the reason why many large companies are looking for Node, its asynchronous nature and non-blocking I/O. With this your server can receive a lot of requests without blocking the application. Node has a library called libuv which is multithreaded, it will handle all the asynchronous processes that Node's single thread cannot and return the response.

Try this code instead:

console.log(fs.readFileSync("./assets/test.txt").toString()); // I'm soooo fast

fs.writeFileSync("./assets/test.txt", "I'm actually faster");
Enter fullscreen mode Exit fullscreen mode

Now you are using the synchronous methods and the code is enclosed in those statements.

FS allows a lot more actions but you have the basic idea, with this module we can, for example, read a file, do some calculations, modify it and return its content to the front-end.

http/http

With these modules we can configure our Node as a HTTP/HTTPS server, this will be the module we will use to create the REST API.

// server.js
const http = require("http");

const HOSTNAME = "localhost";
const PORT = 3000;

const server = http.createServer((req, res) => {
  console.log(req);
  console.log(res);
});

server.listen(PORT, HOSTNAME, () => {
  console.log(`Server started on http://${HOSTNAME}:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

If you use node server and open a browser with localhost:3000 you will see in the server console those console.log which contain two useful parameters: the request and response objects. These objects contain some useful information that we'll look at in detail later, but for now you can take a look at what is printed.

  • We use the built-in http module.
  • The hostname from which the server will respond will be our localhost.
  • As a convention, port 3000 is used for local development, but you can use any port you like if it is available.
  • We use the createServer function.
  • We start the server with listen.

As you can see, the console.log is not printed to the browser console it is only printed to the server console, this is because we are running server code here, in the next section we will see how to send data to the front-end which will be the core of our REST API.

Creating a Server

// server.js
const http = require("http");

const HOSTNAME = "localhost";
const PORT = 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.write("Hello from the Server!");
  res.end();
});

server.listen(PORT, HOSTNAME, () => {
  console.log(`Server started on http://${HOSTNAME}:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Now try accessing localhost:3000 in your browser and check the results.

We set up the server to respond (using the response object) to incoming requests with plain text, indicating a 200 status code and terminating the communication.

If you look closely at the example in the previous section, once you access localhost:3000 the browser never resolves the request, that was because we were not using end to notify the end of the communication.

Status Codes

If you do not know what status codes are see this list, in short the status code serves to notify whether the communication has been successful or what kind of problem has occurred.

Content Type

This header is used to tell the client what is the type of content returned. If you want to check the different types see this list.

Useful external packages

We already saw some useful built-in modules, but the community has developed tons of well-done packages worth mentioning and you will find many when you check the internet.

If you don't already, you can initialize your project with npm in your project folder:

npm init -y
Enter fullscreen mode Exit fullscreen mode

This will generate a simple package.json which will be useful in the next sections and is necessary to install external packages.

nodemon

If you try to modify the code above with the server running, you will probably have noticed that the changes require a restart of the node process. The nodemon external package watches for changes to our files and applies them automatically without the need for a restart.

See the official nodemon page but in short

npm install -D nodemon
Enter fullscreen mode Exit fullscreen mode

Install as a development dependency and configure your start script as follows:

"start": "nodemon server.js"
Enter fullscreen mode Exit fullscreen mode

And execute it:

npm start
Enter fullscreen mode Exit fullscreen mode

Your server will automatically react to changes.

Express

We will see this package in detail in the next section, for now let's say that Express is a web framework for Node, it simplifies the process of developing a web application and aims to build efficient and fast web applications. Express is also the E of the MEAN/MERN/MEVN stack.

You can achieve that result without Express or even with other packages but let's look at the advantages of this particular package.

To add Express to your project:

npm install express
Enter fullscreen mode Exit fullscreen mode

Morgan

Morgan is an external package that is part of Express, this package allows us to log events in an easy and simple way, it is very convenient for these first steps to check what is happening in our server.

In the next section we will see how to use it, for now let's add it to our project:

npm install -D morgan
Enter fullscreen mode Exit fullscreen mode

One tip, when using an external package, even if you have seen it in a tutorial, make sure that it really solves a problem, for example body-parser is a package that is present in almost all such guides but Express really has its own solution nowadays.

Express

As we saw in the last section we will use Express in our project but I think the most important thing when you add a new package to your project is to know why and what problem it actually solves.

We are going to build a simple REST API as an example. You can achieve this behavior without installing Express and just using Node.

First let's create a database folder and a companies.json inside it, this file will act as a simple database.

// companies.json
[
  {
    "id": "0",
    "name": "Capsule Corp",
    "about": "Like WinRAR but we accept more file extensions.",
    "industries": ["automobile", "house", "engineering"],
    "numberEmployees": 2,
    "yearFounded": 1990
  },
  {
    "id": "1",
    "name": "Red Ribbon",
    "about": "We deliver the best Android you can ever had",
    "industries": ["militar", "artificial intelligence", "engineering"],
    "numberEmployees": 2000,
    "yearFounded": 1000
  }
]
Enter fullscreen mode Exit fullscreen mode
// server.js
const fs = require("fs");
const http = require("http");

const HOSTNAME = "localhost";
const PORT = 3000;
const DB_PATH = `${__dirname}/database/companies.json`;

const getCompanies = res => {
  fs.readFile(DB_PATH, (error, data) => {
    if (error) {
      console.error(error);
      res.statusCode = 500;
      res.end();
    } else {
      res.setHeader("Content-Type", "application/json");
      res.statusCode = 200;
      res.end(data);
    }
  });
};

const deleteCompany = (res, id) => {
  fs.readFile(DB_PATH, (error, data) => {
    if (error) {
      console.error(error);
      res.statusCode = 500;
      res.end();
    } else {
      const companies = JSON.parse(data);
      const filteredData = JSON.stringify(
        companies.filter(company => company.id !== id),
        null,
        2
      );

      fs.writeFileSync(DB_PATH, filteredData);

      res.setHeader("Content-Type", "application/json");
      res.statusCode = 200;
      res.end(filteredData);
    }
  });
};

const server = http.createServer((req, res) => {
  const baseURL = "http://" + req.headers.host + "/";
  const url = new URL(req.url, baseURL);

  if (url.pathname === "/companies" && req.method === "GET") {
    getCompanies(res);
  } else if (url.pathname === "/companies" && req.method === "DELETE") {
    deleteCompany(res, url.searchParams.get("id"));
  } else {
    res.statusCode = 404;
    res.end();
  }
});

server.listen(PORT, HOSTNAME, () => {
  console.log(`Server started on http://${HOSTNAME}:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Starting with the createServer as before we set up a server that listens for requests and depending on the URL and method used executes one logic or another.

Within the two different methods we read the JSON file and return the content, in deleteCompany we look for a specific Company and filter the array and write to the file while returning the resulting array.

If you want to try the previous example, I recommend you to use Postman, an application that we will see in detail later, with which you can execute different requests to a specific endpoint using different methods.

As you can see, the REST API above is incomplete, we only have the get, delete and not found endpoints, but it's enough to see some advantages of using Express, so let's compare it with an Express version of the same application.

Create a new file app.js:

// app.js
const express = require("express");
const fs = require("fs");

const HOSTNAME = "localhost";
const PORT = 3000;
const DB_PATH = `${__dirname}/database/companies.json`;
const app = express();

const getCompanies = (req, res) => {
  fs.readFile(DB_PATH, (error, data) => {
    if (error) {
      console.error(error);
      res.status(500).end();
    } else {
      res.status(200).send(JSON.parse(data));
    }
  });
};

const deleteCompany = (req, res) => {
  const { id } = req.params;

  fs.readFile(DB_PATH, (error, data) => {
    if (error) {
      console.error(error);
      res.status(500).end();
    } else {
      const companies = JSON.parse(data);
      const filteredData = JSON.stringify(
        companies.filter(company => company.id !== id),
        null,
        2
      );

      fs.writeFileSync(DB_PATH, filteredData);
      res.status(200).send(JSON.parse(filteredData));
    }
  });
};

app.get("/companies", getCompanies);

app.delete("/companies/:id", deleteCompany);

app.use((req, res) => {
  res.status(404).send("Not found");
});

app.listen(PORT, HOSTNAME, () => {
  console.log("Server running");
});
Enter fullscreen mode Exit fullscreen mode

Let's check the differences between the two versions.

Server listening

The server does not need to specify the default value of localhost.

You can also use an extended version:

app.listen(PORT, HOSTNAME, () => {
  console.log("Server running");
});
Enter fullscreen mode Exit fullscreen mode

Routes

As you can see the routes section is simplified, cleaner and more readable. Each route is declared with a function that uses the same name as the method being used, e.g. the endpoint to list all companies is a get method and the endpoint to delete a particular company is a delete method.

All routes accept a function that receives the request and response objects:

app.get("/companies", (req, res) => {
  // Do something
});
Enter fullscreen mode Exit fullscreen mode

With this in mind we can isolate that logic within a function and pass the function directly:

// app.get("/companies", (req, res) => getCompanies(req, res));
app.get("/companies", getCompanies);
Enter fullscreen mode Exit fullscreen mode

For the deletion endpoint, we need to know the id of the Company, for that we can use identifiers with : those identifiers will travel under req.params.identifierName where identifierName is id in this case.

Finally, in case someone tries to access a route we don't have defined, we define 404 Not Found. The app.use method is a special method that we will cover in the next section.

Response

In the Node version we send back and end the communication with end method which still it's available but Express allows us to do in a simpler way:

res.send(data);
Enter fullscreen mode Exit fullscreen mode

send will automatically set the Content-Type for us.

Status codes

Setting status codes is also easier with Express, most of them will be handled automatically by Express, but if you need to define something explicitly:

res.status(200).send(data);
Enter fullscreen mode Exit fullscreen mode

Middlewares

Remember the app.use we saved for later? Now is the time. Try pasting the app.use lines at the beginning of the file, put them before the other routes and see what happens when you make a request.

// app.js

app.use((req, res) => {
  res.status(404).send("Not found");
});

app.get("/companies", getCompanies);

app.delete("/companies/:id", deleteCompany);

app.listen(PORT, HOSTNAME, () => {
  console.log("Server running");
});
Enter fullscreen mode Exit fullscreen mode

As you can see, now every request is responding with Not found because use is catching all requests and doing an action. Now remove that and try these statements at the top of the file:

// app.js

app.use((req, res, next) => {
  console.log("I'm watching you");
  next();
});

app.get("/companies", getCompanies);

app.delete("/companies/:id", deleteCompany);

app.use((req, res) => {
  res.status(404).send("Not found");
});

app.listen(PORT, HOSTNAME, () => {
  console.log("Server running");
});
Enter fullscreen mode Exit fullscreen mode

Now every request prints I'm watching you first but executes correctly. To understand why this happens you first have to learn about middlewares.

Middleware functions have access to the request and response object and are executed on every execution between a request and a response. If you think about the definition you come to the conclusion that the whole Express is made up of middleware functions, not just app.use.

The difference with other functions like app.get or app.delete is that those functions are limited to those methods, but app.use is executed with any request.

Middleware functions have two possible exits, continue to the next middleware function using next or make a response and terminate the chain.

middlewares on express

In the diagram above you can see the following:

  • A request arrives at the server.
  • The first app.use is executed and performs next.
  • The second app.use is executed and performs next.
  • The request was a get method that asked for the path /, so the app.get executes and sends a response.

Sending a response is what breaks the middleware chain so it is important to note the order.

Built-in middlewares

It is likely that if you are building a front-end that submits data to a REST API, to submit a form for example, you will need to read those values. In the past, to do this we used an external middleware called body.parser to read these values from the req.body. Nowadays this is already integrated in Express and is one of the built-in middlewares.

app.use(express.urlencoded({ extended: true }));
Enter fullscreen mode Exit fullscreen mode

External middlewares

There are lots of external packages for Express, but earlier I mentioned morgan, this package is just an external middleware that if I show you now how to use it you will understand the idea perfectly:

import morgan from "morgan";

app.use(morgan("dev"));
Enter fullscreen mode Exit fullscreen mode

Extending the capabilities of Express with external middleware as you can see is simple and clean.

Best practices

MVC

MVC stands for Model-View-Controller and is a well-established software design pattern in different systems that can be useful here as well. A graphical summary of what MVC is:

mvc-diagram

At this stage of the tutorial we will only use the Controller, the Model we will add later when we define a model for the database and the View in this case is not applicable because we are not serving HTML from the server, the view will be our React application in any case.

Even the lack of certain parts, splitting our code following the MVC pattern is useful for readability and maintainability purposes, so let's isolate all the different functions for manipulating data that we have seen before in the controller.

Under the controller folder, we'll place the company.js and joboffer.js files, with code similar to the following: (check out the example project for the full code)

// controller/company.js
import path from "path";
import fs from "fs";

const DB_PATH = path.resolve("database/companies.json");

const list = (req, res) => {
  fs.readFile(DB_PATH, (error, data) => {
    if (error) {
      console.error(error);
      res.status(500).end();
    } else {
      res.status(200).send(JSON.parse(data));
    }
  });
};

const delete = (req, res) => {
  const { id } = req.params;

  fs.readFile(DB_PATH, (error, data) => {
    if (error) {
      console.error(error);
      res.status(500).end();
    } else {
      const companies = JSON.parse(data);
      const filteredData = JSON.stringify(
        companies.filter(company => company.id !== id),
        null,
        2
      );

      fs.writeFileSync(DB_PATH, filteredData);
      res.status(200).send(JSON.parse(filteredData));
    }
  });
};

export { list, delete }
Enter fullscreen mode Exit fullscreen mode

*The other methods can be found in the example project.

By doing so, we have isolated the code relating to working with the data in a single file, which we can then reuse as needed, as in the next section.

Routes using router

There is a better way to organise the routes, especially now that we want to add another context, so far we only talked about routes about company but now we want to add routes for job offer. Let's use the router to organise the routes in a better way.

Inside the routes folder, we'll place two files company.js and joboffer.js, which will contain something similar to this code: (check the example project for the full code)

// routes/company.js
import express from "express";

import { list, create, details, update, remove } from "../controller/company";

const router = express.Router();

router.get("/", list);
router.post("/", create);
router.get("/find/:id", details);
router.put("/:id", update);
router.delete("/:id", remove);

export default router;
Enter fullscreen mode Exit fullscreen mode

Let's check what happens there:

  • We use the Router function of Express.
  • With the router, we can add routes in the same way as we did with app.
  • Finally we export the router.

Later, we can use this router to define all routes:

import express from "express";

import { companyRoutes, jobOfferRoutes } from "../routes";

const app = express();

// routes
app.use("/company", companyRoutes);
app.use("/job-offer", jobOfferRoutes);
Enter fullscreen mode Exit fullscreen mode

With app.use we define a context for that path (this is entirely optional) and add the paths we defined earlier. The advantage of using the context is that the routes in the example above are simpler and easier to move between contexts.

So instead of declaring all your routes in your app.js or whatever main file you have, isolate them in their own files, it will be easier and less error prone for other developers to modify in the future.

TypeScript

As I said at the beginning of this guide, TS can be useful in this project, and if you check the example project is entery in TS, in later stages of the guide it will be even more useful because of the type checking of the model, but for now here are some benefits:

Clear data structure

// types.ts

type Company = {
  id: string;
  about: string;
  industries: string[];
  name: string;
  numberEmployees: string;
  yearFounded: number;
};

type JobOffer = {
  id: string;
  availablePositions?: number;
  companyId: string;
  description: string;
  function: string;
  industry: string;
  location: string;
  numberApplicants?: number;
  postDate: Date;
  published: boolean;
  requirements: string[];
  salary?: number;
  workType: string;
};

export { Company, JobOffer };

Enter fullscreen mode Exit fullscreen mode

Declaring the types of our objects gives us, and other developers, a snapshot of what we are talking about. Looking at a single file, you now have a clear picture of the form of the data, which parameters are mandatory and which are optional.

This will be even more useful later, but for now we can use these types in the controller to implement less error-prone functions, use IntelliSense efficiently and include these types in our tests.

Readable code

Let's check for an updated version of the remove function in the company's controller:

// controller/company.ts
import { Request, Response } from "express";
import path from "path";
import fs from "fs";

import { Company } from "../types";

const DB_PATH = path.resolve("database/companies.json");

const remove = (req: Request, res: Response) => {
  const { id } = req.params;

  const companies: Company[] = JSON.parse(fs.readFileSync(DB_PATH).toString());
  const company: Company | undefined = companies.find(company => company.id === id);
  const newCompanies: Company[] = companies.filter(company => company.id !== id);

  if (company) {
    fs.writeFile(DB_PATH, JSON.stringify(newCompanies, null, 2), error => {
      if (error) {
        console.error(error);
        res.status(500).end();
      } else {
        res.status(200).send({ message: `Company with id ${id} removed.` });
      }
    });
  } else {
    res.status(404).send({ message: `Company with id ${id} not found.` });
  }
};
Enter fullscreen mode Exit fullscreen mode

Most of the types are inferred and it is not necessary to write it explicitly, but I added it here so that it is better understood that we now know at each step what type of data we are handling and more importantly, the IDE is checking that it follows that form.

Better understand of external tools

Do you see this in the previous example?

import { Request, Response } from "express";

const remove = (req: Request, res: Response) => {}
Enter fullscreen mode Exit fullscreen mode

Good luck finding out what is inside the req and res params, you will need to check the documentation or debug, with TS you will automatically have access to the object form and documentation, directly from the IDE, this is one of the main reasons why I am currently using TS in my projects.

Publish

Let's review what the different options are for publishing our backend so that it is accessible to others, due to the current size of the guide, I will keep this section as a summary but will consider making a more focused guide on this point if I feel it is necessary.

Local

On a basic scale you already have a local environment for your node server but it is not available outside your current local network, with this you may be able to test the server as we saw in the Postman section.

Nowadays it is less common to want to use your local machine as a server, and if you prefer not to do that check the next sections, but if you want to expose your local node server to the world you can use ngrock, the introduction video on the landing page is self explanatory to be honest 😄

AWS

You can use Amazon Web Services to host your Node.js application, I will list the steps but I won't go into the details because using AWS requires some prior knowledge about AWS and is beyond the scope of this guide.

  • Request an Elastic Compute Cloud (EC2) instance with Ubuntu for example.
  • Update the system.
  • Install Node.js on the system as we did in the the Setup section for Ubuntu.
  • Clone your back-end project or the example project from git.
  • Perform npm install && npm start which will make the Node.js server available.

This is a simple step-by-step for this guide, there are actually better ways to deal with disconnects, restarts, and so on, check out pm2 if you are more interested in this part.

Be careful with this option because AWS has a free tier but may have additional charges for usage.

Heroku

One of the easiest options and the one I will cover here in more detail is to use Heroku. Heroku is a Platform as a Service (PaaS) that will simplify the process of having to configure your system to be visible from the outside and act as a server.

One of the cool things about Heroku is that we can do this kind of testing without any kind of credit card or fee, so it's perfect for a guide like this and your first tests developing backends with Node.js.

With the example project, I needed to add a postinstall script for TypeScript so that Heroku compiles down to JS code before starting the server.

There are two ways to upload a back-end project like the example project in this guide:

Heroku CLI

Heroku provides a command line interface that we can use to deploy the project in a few steps. First install the cli directly from npm:

npm install -g heroku
Enter fullscreen mode Exit fullscreen mode

Once installed, we need to log in:

heroku login -i
Enter fullscreen mode Exit fullscreen mode

If you want to verify that everything works before uploading to Heroku, you can check it with:

heroku local web
Enter fullscreen mode Exit fullscreen mode

web will check your package.json and look for the start script.

Once everything is verified, let's create the project in Heroku and push it:

heroku create
git push heroku main
Enter fullscreen mode Exit fullscreen mode

After create you will get the URL where it is stored and you are ready to go, if you are using the example project, you can try with your new url + /company for example. In my case https://mars-pot-backend.herokuapp.com/company.

Directly on the web.

  • Once logged into Heroku, in your dashboard select New and Create new app, you can choose a name and a region.
  • Then you can select your project from github and deploy a specific branch.
  • Once deployed, in Settings you can check the Domains section to see the url of your project, if you are using the example project, you can try your new url + /company for example. In my case https://mars-pot-backend.herokuapp.com/company.

For a successful deployment you must have a start script in your package.json in this case it will be the script to start the node server.

Railway

I found Railway during the process of this guide and I'm quite surprised, I try uploading the example project here and within seconds I have an instance ready to go, even with a provisioned MongoDB available but that's for the next iteration of this guide.

I haven't tested this option in depth but I will try it with future iterations of this series because it seems convenient.

BONUS

Postman

Throughout this guide you can test the different api rest endpoints directly in the browser or using curl but one tool that will make life easier for you and your co-workers is Postman.

One of the main benefits of using Postman with your co-workers or even on side projects for yourself is to easily define how to interact with your API, provide examples, and collaborate in the same workspace to maintain that collection.

There are also plenty of APIs available so you can test how things work and plan how to code anything before you start writing, for example the Twitter API workspace.

Testing endpoints

With the example project I also provide a Postman collection, you can use it as an example for your collection or to test the example project.

If you want to create a bunch of endpoints and test your own application, it's as easy as selecting the request method and url.

postman endpoints

For endpoints that have to carry data to the server, they can be sent via params or the Body.

postman endpoints 2

Postman provides a lot of information about the request and the response, so you won't miss anything from the Developer Tools Network tab:

postman endpoints 3

Creating examples

Providing examples in the Postman collection is a fantastic way to ensure that your colleagues or collaborators can see the shape of the data without actually running anything, in a regular scenario this may not be necessary but when a service is behind a proxy, authentications or even the service is not yet avialable, it can be a good resource for developers to start coding their part.

To create a new example, click on the three dots at the endpoint you want to add an example and select Add example.

postman examples

Environment variables

As in programming, you can isolate your constants in environment variables to share different configurations and make it easier to modify the collection or test endpoints with different environments.

postman environment variables

In the sample project collection you can find variables to run the endpoints on your local or directly to the published version on Heroku. To use the environemnt provided to the Postman collection you must import the two jsons provided in the same folder which are *environment.json.

Discussion (3)

Collapse
tripol profile image
Ekanem

This was a really extensive and enjoyable article. Thanks a lot

Collapse
dastasoft profile image
dastasoft Author

Thanks to you for the comment :)

Collapse
petrof21 profile image
Bojan Petrović

Wow, Awesome! Epic!