Node js Logo Sticker

Introduction

If you want to improve the quality of your JavaScript code by using a typed language, Typescript is the right choice. In this post, you will learn how to set up a Node.js API server project from scratch using Typescript, covering everything from installation to best practices that will make your code more robust and scalable.

1. Why Use Typescript with Node.js?

Typescript offers static typing, which helps prevent common errors that occur during runtime in JavaScript. Additionally, it enhances the development experience with features like autocomplete, type checking, and safer refactoring. This is especially useful in Node.js projects, where maintaining clean and sustainable code is essential.

2. Installing Node.js and NPM

Before you start, you need to ensure that Node.js and NPM are installed on your machine. Follow the steps below:

Check if Node is installed on your computer:

node -v

Install Node.js if necessary:

Download Node.js

3. Initializing the Node.js Project

Create a new folder for your project and initialize a new Node.js project:

mkdir api-node-ts
cd api-node-ts
npm init -y

The command npm init -y creates a package.json file with the default configuration.

4. Setting Up Typescript

Now, let’s install Typescript and some essential dependencies:

npm install typescript ts-node @types/node --save-dev

After the installation, create a Typescript configuration file:

npx tsc --init

This command generates a tsconfig.json file in the root of the project, where you can configure options like the ECMAScript version and the output directory.

Replace the content of the tsconfig.json file with the following:

{
    "compilerOptions": {
        "target": "es6",
        "module": "commonjs",
        "outDir": "./dist",
        "rootDir": "./src",
        "strict": true,
        "moduleResolution": "node",
        "esModuleInterop": true,
        "resolveJsonModule": true,
        "allowSyntheticDefaultImports": true
    },
    "include": [
        "src/**/*.ts"
    ],
    "exclude": [
        "node_modules"
    ]
}

The compilerOptions section contains the necessary information for compilation, with the following configurations:

  • target: Indicates which ECMAScript version the code should be transpiled to. Even though the code is written in Typescript, during the transpilation process, it will be published as JavaScript. In this case, we’re using es6 (ECMAScript 6).
  • module: Indicates which module system will be used in the project, affecting how files can be exported and imported.
  • outDir: Specifies the folder where the transpiled .js code will be placed.
  • rootDir: Indicates the main directory containing the folder structure that will be copied into the directory specified in outDir.
  • strict: Enables a set of automatic checks in the code, providing more security.
  • moduleResolution: Along with the module parameter, indicates the algorithm used for module resolution.
  • esModuleInterop: Allows importing CommonJS modules in a way that’s compatible with ES6.
  • resolveJsonModule: Allows importing .json files just like modules, without needing to use fetch.
  • allowSyntheticDefaultImports: Allows the import of modules that don’t have a default export.

The include section specifies which directories and files will be included in the transpilation.

The exclude section specifies which directories and files should be excluded from the transpilation.

5. Creating the Project Structure

Organize your project’s folder and file structure to facilitate maintenance and scalability. We’ll dive deeper into the benefits of this strategy later:

api-node-ts/
│
├── src/
│   └── app/
│   │   └── controllers/
│   │   │   └── test.controller.ts
│   └── app.ts
│   └── routes.ts
│   └── server.ts
├── dist/
├── .env
├── .gitignore
├── package.json
└── tsconfig.json

6. Setting Up the Server with Express

Express is a minimalist framework that makes it easier to create HTTP servers. Let’s set it up with Typescript.

Install Express and its types:

npm install express
npm install @types/express --save-dev

7. Setting Up Hot Reload

During API development in Node.js, it’s useful for the server to automatically update whenever the code is changed. Otherwise, you’d need to restart the server every time you want to see the implemented changes. This behavior is called Hot Reload, and to enable it, install the following dependency:

npm install ts-node-dev --save-dev

8. Setting Up Environment Variables During Development

It’s a good practice to separate variables that depend on the environment in which the application is running. For example, if you need to configure the port where your application will be available, this value is likely to be different when you’re testing the application on your computer versus when it’s running in production. In such cases, we can use a file called .env, where the variables used during development will be stored. Thus, when the application is moved to production, this variable can be changed without needing to create conditionals in the code.

To provide this support, install the appropriate package:

npm install dotenv

9. Enabling CORS (Cross-Origin Resource Sharing)

A well-known issue that everyone has encountered at least once is trying to access an API and receiving a CORS error, meaning your application doesn’t allow access to its resources from a different domain, port, or protocol.

Imagine a frontend application trying to access this API. Obviously, the frontend will be available on a different domain or port, so you need to inform Node.js that you want to allow this type of access. To do this, import the CORS library:

npm install cors

10. Automating the Process with npm Scripts

To simplify development, we can add some commands to the package.json file that make it easier to execute and test the code.

Open the package.json file, locate the section called scripts (if it doesn’t exist, just add it), and then include the following commands:

"start": "ts-node-dev --respawn ./src/server.ts",
"build": "tsc -p ."

With this section complete, it should look like this:

"scripts": {
    "start": "ts-node-dev --respawn ./src/server.ts",
    "build": "tsc -p ."
}

This way, when you want to start your server, instead of typing the entire command, you can just type the script name in the terminal:

npm start

For commands other than start, like build, you need to use the run flag, for example:

npm run build

The build script only performs the application’s transpilation but doesn’t start the server. This is useful when development is complete and you want to publish the application to a production environment.

11. Preparing the Project for Repository Storage

If you want to save your project in a code repository like GitHub, GitLab, Azure DevOps, etc., there’s a very important step that, if not done now, can cause some headaches later: setting up the .gitignore file.

In step 5, the project structure was shown. If you paid attention, a file called .gitignore was included. This file is used to indicate that there are files we don’t want to version in the repository, such as automatically generated files—in our case, the node_modules folder (which is generated automatically when we install an npm package) or the files generated by the transpilation in the dist folder.

It doesn’t make sense to store these files because the application will always generate them as the project grows. To avoid this, place the following content in this file:

logs
*.log
npm-debug.log*
node_modules/
jspm_packages/
typings/
.npm
.eslintcache
.env
.env.test
.cache
dist

11. Writing Some Code

Now that everything is set up, let’s write the code itself. Remember, this project is a template for creating APIs, so you can start with it and adapt it as needed.

test.controller.ts File

This file will expose a sample endpoint. You can add as many controllers and as many endpoints per controller as you like. Later, we’ll show how to map routes so that each controller serves a specific URI.

Start by adding the following content to the src/app/controllers/test.controller.ts file:

class TestController {
    async get(req: any, res: any) {
        res.send('API Test OK!')
    }
}

export default new TestController();

First, we create the class, and then we create an endpoint called get. The req and res parameters are standard; in req, you can get the input parameters, while res is where we’ll put our API’s response.

Note that we used async to indicate that it’s an asynchronous method, and then we immediately return a message: “API Test OK.”

Finally, we export an instance of this controller as the default so it can be imported by another file later.

routes.ts File

Now that we’ve created the controller, we need to tell Express that we want to map a URI to the get method of the controller. To do this, place the following content in the src/routes.ts file:

import { Router } from 'express';
import testController from './app/controllers/test.controller';

const routes = Router();

routes.get('/', testController.get);

export default routes;

First, we import Router from Express, then we import our newly created controller, create an instance of Router to store all our endpoints, and then create an HTTP GET method using routes.get, specifying that when the user accesses the / URI, we want the call to be processed by the get method we created in our controller.

Finally, we export our routes object.

app.ts File

We need to bring together all the pieces we’ve built so far. To do this, use the following code in the src/app.ts file:

import express from 'express';
import cors from 'cors';
import routes from './routes';

export class App {
    public server;

    constructor() {
        this.server = express();
        this.middleware();
        this.routing();
    }

    private middleware() {
        this.server.use(express.json({ limit: '1mb' }));
        this.server.use(express.urlencoded({ limit: '1mb' }));
        this.server.use(express.json());
        this.server.use((req, res, next) => {
            res.header("Access-Control-Allow-Origin", "*");            
            res.header("Access-Control-Allow-Methods", 'GET,PUT,POST,DELETE');  
            this.server.use(cors());
            next();
        });
    }

    private routing() {
        this.server.use(routes);
    }
}

Here, we import Express and CORS, as well as our routes file.

We create the App class and perform some configurations in its constructor. First, we instantiate the Express server and set the payload limit for the application to 1 MB. By default, Express limits both JSON and URL-encoded payloads to 100 KB, but sometimes this value is not sufficient. In this project, this configuration is not mandatory but is included in the example due to its common nature.

We also configure headers to specify which HTTP methods we want to accept in our application and which origins are allowed. Note that CORS is also enabled here.

We centralize all request and response configurations in a middleware to keep the code organized. By default, every middleware needs to call the next() method at the end of its execution.

Finally, we use our routes, informing Express that we want to use our routes object.

server.ts File

This file is responsible for initializing our server. Place the following content in the src/server.ts file:

import { App } from './app';
import * as http from 'http';
import dotenv from 'dotenv';

dotenv.config();

const PORT = process.env.PORT || 5005;

http.createServer(new App().server).listen(PORT, () => {
  console.log(`==== Listening on port ${PORT}\n`);
});

Here, we use the built-in HTTP module in Node to configure both the protocol (HTTP) and the port where our application will listen for requests.

Note that we use the dotenv package, but we do not explicitly call the .env file because it is handled automatically.

.env File

Lastly, open the .env file and add the following content:

PORT=5005

This informs dotenv that, when running the application, we want it to start the server on port 5005.

12. Compiling and Running the Project

To compile the project, open the terminal and type:

npm run build

You will see that the transpiled .js files have been added to the dist directory.

To run the application, use the command:

npm start

Then open your browser and go to localhost:5005.

13. Download

You can download this project directly from our Github.

Conclusion

You have learned how to create a Node.js project using TypeScript, from setting up the environment and project structure to creating a server with Express and applying best practices. With TypeScript, you can write more secure and maintainable code, leveraging the best of both worlds: JavaScript’s flexibility and static typing’s robustness.

Did you enjoy the content? Leave a comment and share your experiences with TypeScript and Express!

0 Comments

Leave a comment

Your email address will not be published. Required fields are marked *