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.
Table of Contents
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:
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 usefetch
. - 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