This is one of our practical posts. In this post, we will be writing a minimal server with Node.js and TypeScript. This is one of the most effective ways to become familiar with backend development in a typed JavaScript environment. This article outlines the process of setting up a simple Express application that exposes a single /ping endpoint, which replies with “pong”. While this example is intentionally minimal, it introduces several foundational concepts: project setup, TypeScript configuration, Express usage, and file structuring.
To begin, create a new folder for your project and navigate into it. Use the appropriate command for your operating system:
mkdir ts-node-express && cd ts-node-express
Once inside the project directory, initialize an npm project. This generates a package.json file, which serves as the metadata and dependency manifest for the application:
npm init -y
Next, install the dependencies required for building and running a Node.js app with TypeScript and Express. Start by installing Express as a runtime dependency:
npm install express
Then install the necessary development dependencies. These include TypeScript itself, ts-node to allow running TypeScript files without compiling them manually, and the type definitions for Node.js and Express:
npm install --save-dev typescript ts-node @types/node @types/express
Before writing any code, TypeScript needs to be configured. Run the following command to generate a tsconfig.json file:
npx tsc --init
This creates a default configuration. Open tsconfig.json and make a few key changes. Set the “rootDir” to “src” and “outDir” to “dist” so that your source files and compiled output remain separated. Also enable strict typing by setting “strict”: true. These adjustments ensure a clean and predictable development environment, particularly helpful in larger applications.
Now create the folder that will contain the source code:
mkdir src
Within the src folder, create the entry point file for your application. It’s name canbe index.ts. This index.ts file will contain the Express server logic. Open it and insert the following content:
import express from 'express';
const app = express();
const port = 3000;
app.get('/ping', (_req, res) => {
res.send('pong');
});
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});
This snippet sets up a basic Express application. It creates a server listening on port 3000, with a single route: a GET request to /ping returns the string “pong”. The underscore prefix in _req signals that the request object is unused in the handler.
To streamline development, add a start script to your package.json so that you can run the server directly:
"scripts": {
"start": "ts-node src/index.ts"
}
Before starting, you need to pay attantion to a few details. In your package.json, make sure "type": "module", is set at the root. For example:
{
"name": "ts-node-express",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^5.1.0"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/node": "^24.3.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
}
}
Second thing is, make sure you have rootDir and outDir configured in your tsconfig.json file. It should liik like this:
{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
"rootDir": "./src",
"outDir": "./dist",
// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "nodenext",
"target": "esnext",
"types": [],
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
}
}
Now start the application using the following command:
npm start
If everything is configured correctly, the terminal should output a message indicating the server is listening. Navigating to http://localhost:3000/ping in a browser or using curl will return “pong”.
Although this example is intentionally limited in scope, it establishes a pattern that can be extended easily. For larger projects, you might introduce routing layers, dependency injection, environment-specific configuration, or middleware for logging and validation. But having a working, minimal base is always the best place to start. It ensures that each additional step builds on something understandable and reliable.
A lightweight setup like this allows developers to grow their applications incrementally. It also reinforces a core principle of software engineering: simplicity enables clarity, and clarity scales.
Suleyman Cabir Ataman, PhD