Project skeleton
We will assume that the project will have several smaller projects, ie. container, e.g. a part for microservices, a part for working with the database… It is good to create subfolders in the root of the project for each smaller project, e.g.:
|
1 2 3 |
node_project/ ├── api/ # Ovaj za mikroservise ├── data/ # Ovaj folder za bazu podataka |
Creating package.json
First we will create a package.json file in the root folder of the project. This file will contain all the necessary information about the project, such as name, version, dependencies… To create the basic version of package.json, we will use the npm init command with the flag “YES” “-y” to fill everything by itself without asking:
|
1 2 |
cd api npm init -y |
After that the package.json file will look like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "name": "api", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } |
For further work, we need to create and define the initial JS file. We will create the initial JS file in the “src” folder, which we will place in the “api” folder (eg app.js). After that, we need to modify the location and name of the initial file as part of package.json:
|
1 |
"main": "src/app.js" |
Typscript support
-
TypeScript
First we need to install TypeScript itself:
1npm install typescript --save-dev -
tsconfig.json
The
tsconfig.json file is used to configure the TypeScript compiler (tsc). Used to define various compile options that control how TypeScript code will be translated to JavaScript. You can initialize this file with the command
1npx tsc --initExample
12345678910111213141516171819202122{"compilerOptions": {"target": "ES6", // Ciljni ECMAScript standard"module": "commonjs", // The module system used"strict": true, // Enables strict mode"esModuleInterop": true, // It helps with interoperability with ECMAScript modules"skipLibCheck": true, // Skips type checking in definition files"outDir": "./dist", // Output directory for compiled files"rootDir": "./src", // Root directory for input files"resolveJsonModule": true, // Allows import of JSON files as modules"sourceMap": true, // Generates sourcemap files"noImplicitAny": true, // Disables the implicit "any" type"moduleResolution": "node", // Module resolution strategy used"baseUrl": ".", // Osnovna path za rezoluciju modula"paths": { // Mape za rezoluciju modula"@app/*": ["src/app/*"],"@config/*": ["src/config/*"]}},"include": ["src"], // Directories/files to include in compilation"exclude": ["node_modules", "dist", "test"] // Directories/files to exclude from compilation} -
ts-node
In order to run TypeScript directly from the Node environment without the need to compile it to JS beforehand, it is necessary to install the ts-node package:
1npm install ts-node --save-devNext we create a simple “start” script to run the application in production:
123"scripts": {"start": "ts-node src/app.ts"}Invoking the command from the terminal starts the application:
1npm start -
@types packages
When you use JavaScript libraries in a TypeScript project, you need to install the appropriate types in order to debug at the compilation level. JavaScript is a dynamically typed language, which means that there are no built-in types, and in order for TypeScript to know which types should be used for Node.js (written in JS), we need a list of those definitions, which can be found in the package @types/node.
1npm install @types/node --save-devIf, for example, we’re using the “fs” module, without the @types/node package, the TypeScript compiler wouldn’t know anything about it, leading to compile-time errors. The same goes for Express, TypeScript doesn’t know what types it should use for variables written in Express, so we need to help it not throw compile errors by installing the package @types/express
1npm install @types/express --save-dev -
Nodemon
The nodemon package allows us to automatically restart the application on the server when it detects changes in the files in the project, and is installed as follows:
1npm install --save-dev nodemon
Defining scripts
If we have the nodemon package installed, then we can create a script with which we will restart the server every time there is a code change. That script is usually called “dev” because it is used locally during development:
|
1 2 3 |
"scripts": { dev": "nodemon --watch src --exec ts-node src/app.ts" } |
This script is called from the terminal as follows:.
|
1 |
npm run dev |
After starting the script nodemon monitors the changes in the files inside the src directory and if it notices them, it automatically restarts the src/app.ts file using the ts-node library, compiling the TypeScript code “on the fly”.
NOTE:
For building JSproject, we can also create a script, e.g.
"build": "tsc". This command will start the TypeScript compiler that will translate all .ts files into .js files according to the configuration defined in the tsconfig.json file and place them in the output directory (usually dist or build). The script is called as follows:.
|
1 |
npm run build |
ENV variables
In the node.js environment, as in other systems, environment variables gathered in one file are used, with the use of which we avoid hard-coded data scattered in the source code in different places, that file is marked with .env and we create it in the root folder of the project. So the structure of the project will look like this:
|
1 2 3 4 5 6 |
node_project/ ├── api/ │ ├── src/ │ │ └── app.ts │ ├── .env │ ├── package.json |
To load (environment) variables from the .env file into process.env, we need to install the dotenv package:
|
1 |
npm install dotenv |
It is necessary at the beginning of code execution (eg in the initial file) to call the config() method from the DotEnv package because it loads the contents of the .env file and adds each variable to the “process.env” object. Read more about env variables in the article Environment Variables.
NOTE:
The .env file should be added to the .gitignore file to avoid sharing it with version controls, thus protecting sensitive information. Please note that in production, don’t forget to “manually” create a new .env file because it is not shipped with git !!!
Dockerization
Dockerfile
Dockerfile is a text file that contains instructions for creating a Docker image (eng. image). A Dockerfile contains a series of instructions that determine how a Docker image will be created.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# The base images for the container are downloaded from DockerHub FROM node:20-alpine # We set the working directory in the container WORKDIR /user/src/app # We copy package.json and package-lock.json to the working directory # (./ is the working directory ie app) COPY package*.json ./ # We install dependencies from the package.json file only when in production # using "clean install" (npm ci) which first deletes the node_modules folder RUN npm ci --only=production # We copy all the files from the current directory to the working directory COPY . . CMD [ "npm", "run", "dev" ] |
Read more about Docker and its usage in the article “Docker: Running Apps Everywhere”

Image creation (“build”)
The docker “build” command is used to create a Docker image from a Dockerfile. This command has several options that allow you to customize the build process. A detailed explanation of the options commonly used with the docker build command is as follows:
|
1 |
docker build [OPTIONS] PATH | URL | - |
Where PATH is the directory containing the Dockerfile, URL can be a URL to a GIT repository, and “-“ allows the Dockerfile to be read from standard input. Here is a list of some options:
- –tag abbreviated -t: Image tag. Allows image naming and version (tag) assignment.
1docker build -t my-image:latest . - –file abbreviated -f: Specifies the path to the Dockerfile if it is not in the current directory.
1docker build -f /path/to/Dockerfile . - –build-arg: Pass variable as build argument. These variables can be used within the Dockerfile.
1docker build --build-arg HTTP_PROXY=http://proxy.example.com . - –no-cache: Disables the use of the build cache, ensuring that each layer is rebuilt.
1docker build --no-cache -t my-image:latest . - –pull: Always pull the latest base image before building.
1docker build --pull -t my-image:latest . - –label: Adds metadata (labels) to the image.
1docker build --label version="1.0" --label maintainer="you@example.com" .
NOTE:
When Docker builds an image, it needs to have access to all files listed or used within the Dockerfile. These files are located in a directory called “context directory”. The dot (“.”) at the end of the docker build command simply means “use current directory as context directory”. This allows Docker to read the Dockerfile and any required resources from that directory to create the image.
Example:
The dot (.) in the
docker build -f /path/to/Dockerfile .command indicates the current directory as the context directory. This means that Docker uses all files from that directory in the build process, while Dockerfile can be specified anywhere on the system using the -f option.
Example
|
1 |
docker build -t my-image:1.0 -f /path/to/Dockerfile --build-arg HTTP_PROXY=http://proxy.example.com --no-cache --pull --label version="1.0" --label maintainer="you@example.com" . |
This command will:
- Create an image named my-image with tag (image version) 1.0
- Use Dockerfile from specified path
- Set build argument HTTP_PROXY
- Perform build without using cache
- Always download the latest base image
- Add two labels to the image
- The context directory is the current directory
Before starting the “build” command in Windows, we need to start the Docker desktop application, and it is preferable to be in the terminal in the root of the project where the Dockerfile is located.
After the executed command, you can check the image in the “Docker deskop” application, but you can also do the same through the terminal with the command:
|
1 |
docker images |
Docker scripts
If we want to speed up work with Docker, we can prepare scripts in package.json like e.g.
|
1 2 3 |
"docker:build": "docker build -t starter_node:latest .", "docker:run": "docker run --name node_api starter_node:latest", "docker:stop": "docker stop node_api && docker container prune -f" |
Explained
- The docker:build script creates a Docker image from a Dockerfile, named “starter_node:latest”
- The script docker:run starts a new container, assigning it the name “node_api” based on the image “starter_node”
- The script docker:stop stops the container named “node_api”, then removes all stopped containers without asking for confirmation
For more about docker commands, see the article Docker: Running apps everywhere
SUMMARY
This is what the project structure looks like:
|
1 2 3 4 5 6 7 8 9 10 |
node_project/ ├── api/ │ ├── src/ │ │ └── app.ts │ ├── dist/ │ │ └── app.js │ ├── .env │ ├── package.json │ └── Dockerfile ├── data/ # Ovaj folder za bazu podataka |
And this package.json file:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{ "name": "api", "version": "1.0.0", "description": "", "main": "src/app.ts", "scripts": { "start": "ts-node src/app.ts --env-file=.env.production", "build": "tsc", "dev": "nodemon --watch src --exec ts-node src/app.ts --env-file=.env.development", "docker:build": "docker build -t starter_node:latest .", "docker:run": "docker run --name node_api starter_node:latest", "docker:stop": "docker stop node_api && docker container prune -f" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "dotenv": "^16.4.5", "express": "^4.19.2", "nodemon": "^3.1.0" }, "devDependencies": { "@types/express": "^4.17.21", "@types/node": "^20.12.12", "ts-node": "^10.9.2", "typescript": "^5.4.5" } } |
View the complete starter project code on GitHub https://github.com/choslee/node_typescript_doceker_starter
