Create a Hello World REST API in TypeScript Using Node.js and Express

Create a Hello World REST API in TypeScript Using Node.js and Express

If you’re building your first API, the best place to start is with a Hello World REST API: a tiny service that responds to an HTTP request with a simple JSON message. It may look trivial, but it teaches the most important parts of backend development at once: routing, request/response handling, local development, typing, error handling, and testing.

TypeScript is a smart default for new APIs because it adds static typing on top of JavaScript, which helps catch bugs earlier, improves editor autocomplete, and makes code easier to refactor as the project grows. The TypeScript language is designed for scalable JavaScript application development and is widely used in modern Node.js backends. Express remains one of the most common lightweight web frameworks for Node.js, making it a practical choice for simple and production-ready REST APIs alike. [TypeScript Handbook] [Express] [Node.js Docs]

In this guide, you’ll learn how to create a clean, minimal REST API using Node.js, Express, and TypeScript, then evolve it into a project structure that’s ready for real-world development.

 

Hello World API development flow

Introduction: What a Hello World API Is and Why TypeScript Is a Smart Default for New APIs

A Hello World API is the backend equivalent of a first print statement. Instead of printing text to a terminal, the server listens for an HTTP request and returns a response, often in JSON. For example, a GET / request might return:

{ "message": "Hello, world!" }

That response may seem basic, but it confirms that your server is running, your route is wired up correctly, and your client can communicate with it over HTTP. It is the smallest useful proof that the full request lifecycle works.

TypeScript is a smart default for new APIs because backend code tends to grow quickly. Even a small API often accumulates request validation, database access, authentication, environment variables, and integrations. TypeScript helps you keep these pieces organized by adding types for request payloads, route parameters, shared domain models, and function contracts. That means fewer runtime surprises and clearer intent in the code. The TypeScript compiler can also catch type mismatches before deployment, which is especially valuable in server applications where failures can affect multiple users. [TypeScript Handbook]

Another advantage is that TypeScript works naturally with the Node.js ecosystem. Many popular packages, including Express, offer TypeScript-friendly usage patterns and community type definitions. This makes TypeScript a strong choice whether you are building a quick prototype or a service you expect to maintain long term. [Express] [Node.js Docs]

Prerequisites and Setup: Node.js, npm, TypeScript, and Choosing a Framework or Runtime

Before you start, make sure you have a recent version of Node.js and npm installed. Node.js provides the JavaScript runtime for your API, and npm is the default package manager used to install dependencies and run scripts. You can verify both tools with:

node -v
npm -v

For this tutorial, we’ll use Express as the framework because it is simple, familiar, and well-documented. Express is a strong choice when you want a minimal API surface and full control over your app structure. If you prefer a different style, there are other valid options in the Node ecosystem, such as Fastify or NestJS, but Express is ideal for learning the fundamentals first. [Express]

You will also need TypeScript, which compiles your .ts files into JavaScript that Node.js can execute. In a modern setup, many developers use tsx or ts-node for direct execution during development. tsx is often preferred for fast iteration because it can run TypeScript files without a separate build step during local development, while ts-node is another common option for executing TypeScript directly. [tsx] [ts-node]

A good beginner stack for a Hello World REST API looks like this:

  • Node.js: runtime

  • npm: package management and scripts

  • TypeScript: type system and compiler

  • Express: routing and HTTP server abstraction

  • tsx or ts-node: local development runtime

  • nodemon or built-in watch mode from your dev tool: automatic restarts during edits

This combination gives you a clean learning path with enough structure to be useful in real projects.

Project Initialization: Create the Folder, Install Dependencies, and Generate tsconfig.json

Start by creating a new project folder and initializing npm:

mkdir hello-world-api
cd hello-world-api
npm init -y

Next, install Express and the development dependencies you need for TypeScript:

npm install express
npm install -D typescript @types/node @types/express tsx

Here’s what each package does:

  • express: the web framework

  • typescript: the TypeScript compiler

  • @types/node: type definitions for Node.js APIs

  • @types/express: type definitions for Express

  • tsx: run TypeScript files directly in development

Now generate a tsconfig.json file:

npx tsc --init

A practical starter tsconfig.json might look like this:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

This configuration sets a modern JavaScript target, uses Node’s ESM-aware module resolution, keeps your source files in src, and emits compiled output to dist. The strict option is especially useful because it enables stronger type checking and helps catch problems early. [TypeScript TSConfig Reference] [Node.js ECMAScript Modules]

Create the src directory and a basic entry file:

mkdir src
touch src/index.ts

A clean project structure from the beginning reduces confusion later, especially when the API grows beyond a single file.

Minimal API Implementation: Define a Basic Route, Request Handler, and Response

Now let’s build the smallest possible working API. In src/index.ts, write:

import express, { Request, Response } from "express";

const app = express();
const port = 3000;

app.get("/", (req: Request, res: Response) => {
  res.json({ message: "Hello, world!" });
});

app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
});

This code does three essential things:

  1. Creates an Express application instance.

  2. Defines a GET / route.

  3. Starts the server on port 3000.

The route handler receives a request and response object. In Express, these are strongly typed when you import Request and Response from the package definitions. The handler uses res.json(...) to return a JSON response, which is the standard format for many REST APIs.

You can also add a health endpoint, which is commonly used in real services:

app.get("/health", (req: Request, res: Response) => {
  res.status(200).json({ status: "ok" });
});

A health check route is useful for manual inspection, uptime monitoring, and deployment systems. Even though this is a simple example, it already reflects a production-friendly pattern: one route for the main message, another for service status.

Running Locally and Developer Workflow: tsx, Watch Mode, and Live Reload

A good developer workflow makes the project enjoyable to work on. Instead of compiling TypeScript manually after every change, you can run the app directly with tsx. Add these scripts to package.json:

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Now you can start the development server with:

npm run dev

Using watch mode means the server automatically restarts when you edit files. This is one of the biggest quality-of-life improvements for API development because you can change code and immediately retest routes.

If you prefer ts-node, the development command would look slightly different, but the idea is the same: run TypeScript directly without a manual compile step. tsx and ts-node both support iterative development, though tsx is often chosen for its speed and straightforward setup. [tsx] [ts-node]

For a more polished workflow, you might also add live reload tooling in the browser if your API includes a frontend. For backend-only work, automatic server restarts are usually enough. You can test the endpoint in your browser by opening:

http://localhost:3000/

Or by using curl:

curl http://localhost:3000/

That should return the JSON greeting. At this stage, your API is fully functional, even though it is extremely small.

 

Developer workflow and watch mode

Adding Structure: Organizing Routes, Controllers, and Services for Maintainability

A single-file API is fine for a tutorial, but real projects grow fast. The moment you add more than a couple of endpoints, it becomes helpful to separate responsibilities into routes, controllers, and services.

A simple structure might look like this:

src/
  app.ts
  server.ts
  routes/
    hello.routes.ts
  controllers/
    hello.controller.ts
  services/
    hello.service.ts

Here is what each layer does:

  • Routes define URL paths and HTTP methods.

  • Controllers handle request/response logic.

  • Services contain business logic or reusable operations.

For example, src/services/hello.service.ts could export a function:

export function getHelloMessage(): string {
  return "Hello, world!";
}

Then src/controllers/hello.controller.ts might use that function:

import { Request, Response } from "express";
import { getHelloMessage } from "../services/hello.service.js";

export function helloController(req: Request, res: Response) {
  res.json({ message: getHelloMessage() });
}

And src/routes/hello.routes.ts would wire it together:

import { Router } from "express";
import { helloController } from "../controllers/hello.controller.js";

const router = Router();

router.get("/", helloController);

export default router;

Finally, src/app.ts mounts the route:

import express from "express";
import helloRoutes from "./routes/hello.routes.js";

const app = express();

app.use(express.json());
app.use("/", helloRoutes);

export default app;

Then src/server.ts starts the process:

import app from "./app.js";

const port = 3000;

app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
});

This separation improves readability, testability, and maintainability. It also makes it easier for a team to work on different parts of the API without stepping on each other’s toes.

Type Safety Essentials: Typing Request and Response Objects, Environment Variables, and Shared Interfaces

TypeScript becomes especially valuable when you use it to describe real API data. One of the simplest ways to improve safety is to type your request and response objects properly.

For example, if you expect a JSON body like this:

{ "name": "Ada" }

you can define an interface:

interface GreetingRequestBody {
  name: string;
}

Then use it in a route handler:

import { Request, Response } from "express";

export function greetController(
  req: Request<{}, {}, GreetingRequestBody>,
  res: Response
) {
  res.json({ message: `Hello, ${req.body.name}!` });
}

This helps document the expected payload and gives you type-aware access to req.body.name.

Environment variables should also be typed carefully. In many APIs, values like PORT, NODE_ENV, or DATABASE_URL come from the environment rather than hardcoded configuration. Node.js exposes these through process.env, but their values are technically strings or undefined. [Node.js Process Env]

A small helper can make this safer:

function getEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Missing environment variable: ${name}`);
  }
  return value;
}

Then:

const port = Number(getEnv("PORT"));

For larger projects, you may want a shared types/ directory for interface definitions such as User, ApiError, or HealthStatus. Shared interfaces reduce duplication and keep your models consistent across controllers, services, and tests.

Good typing does not eliminate runtime bugs, but it dramatically narrows the space where they can hide.

Error Handling and Validation: Basic Middleware, Status Codes, and Safe Failure Responses

A professional API should fail gracefully. Instead of crashing or returning vague errors, it should respond with the right HTTP status code and a safe message.

Basic error handling in Express often uses middleware. A simple error middleware might look like this:

import { Request, Response, NextFunction } from "express";

export function errorHandler(
  err: unknown,
  req: Request,
  res: Response,
  next: NextFunction
) {
  console.error(err);

  res.status(500).json({
    error: "Internal Server Error"
  });
}

If a route has validation needs, check inputs before using them. For example:

import { Request, Response } from "express";

export function greetController(req: Request, res: Response) {
  const { name } = req.body as { name?: string };

  if (!name || typeof name !== "string") {
    return res.status(400).json({
      error: "name is required and must be a string"
    });
  }

  res.json({ message: `Hello, ${name}!` });
}

This is a simple manual validation approach. In larger applications, developers often use validation libraries to keep rules consistent and expressive, but even basic checks are a big improvement over assuming input is always correct.

Use status codes intentionally:

  • 200 OK for successful reads

  • 201 Created for successful resource creation

  • 400 Bad Request for invalid input

  • 404 Not Found for unknown routes or missing resources

  • 500 Internal Server Error for unexpected failures

Safe failure responses are important because they help clients understand what happened without exposing implementation details.

Testing and Verification: curl, Browser Checks, and Simple Automated Tests with a Test Runner

Once your API works locally, verify it in more than one way. Start with a browser or curl check for the simplest manual confirmation.

For example:

curl http://localhost:3000/

You should see your JSON response. If you added a health endpoint:

curl http://localhost:3000/health

That confirms the route responds correctly and the server is listening.

For automated testing, use a test runner such as Vitest or Jest. A lightweight test stack is usually enough for a Hello World API and small route tests. Here is a simple example using Vitest and Supertest:

npm install -D vitest supertest @types/supertest

A test might look like this:

import request from "supertest";
import app from "../src/app.js";
import { describe, it, expect } from "vitest";

describe("GET /", () => {
  it("returns hello world", async () => {
    const response = await request(app).get("/");

    expect(response.status).toBe(200);
    expect(response.body).toEqual({ message: "Hello, world!" });
  });
});

This kind of test is useful because it exercises the route through HTTP rather than calling a function directly. That gives you more confidence that routing, middleware, and JSON output are all behaving as expected.

Add a test script:

{
  "scripts": {
    "test": "vitest"
  }
}

Automated tests are one of the best habits you can build early. Even a tiny API benefits from them because they turn “it works on my machine” into repeatable proof.

Next Steps and Production Readiness: Logging, Linting, Build Scripts, Deployment, and Future Enhancements

A Hello World API is only the beginning. To make it production-ready, you need a few extra pieces that improve reliability and maintainability.

First, add logging. In development, console.log is fine, but real services usually need structured logging that captures timestamps, severity levels, and request context. This becomes especially helpful when diagnosing errors in production.

Second, add linting and formatting. Tools like ESLint help enforce consistent code quality and catch common mistakes before they reach runtime. Combined with Prettier, they reduce style debates and keep the codebase readable.

Third, refine your build process. A typical production workflow is:

npm run build
npm start

The build step compiles TypeScript into JavaScript, and the start command runs the compiled output from dist/. This makes deployment more predictable because production does not need to transpile code on the fly.

Fourth, think about deployment. Your API can be deployed to a container platform, a serverless environment, or a traditional Node.js host. Whatever platform you choose, make sure it supports environment variables, startup health checks, and HTTPS termination.

Finally, consider future enhancements such as:

  • request validation with schemas

  • API versioning

  • authentication and authorization

  • database integration

  • rate limiting

  • caching

  • OpenAPI/Swagger documentation

Even if your current API is just a Hello World example, the structure you choose today can make all of those future additions much easier.

 

API architecture and growth path

Conclusion

Building a Hello World REST API in TypeScript with Node.js and Express is one of the best ways to learn modern backend fundamentals. You start with a single route, but along the way you also learn how to initialize a project, type your handlers, run the server locally, organize code for scale, validate input, handle errors, and test your endpoints.

The main takeaway is that TypeScript gives you a safer default for API development, especially when your project is expected to grow. Express keeps the runtime simple, Node.js provides a stable foundation, and a small amount of structure early on pays off later in maintainability and confidence.

From here, you can expand the API into something much larger: a CRUD service, an authentication backend, or a production microservice. The Hello World endpoint is just the beginning—but it’s the right beginning.

References

 

Create a Hello World REST API in TypeScript Using Node.js and Express | PageCraft