Skip to main content

Module 14: TypeScript with Node.js

Learn to build type-safe server-side applications with TypeScript and Node.js, covering Express, middleware, async patterns, and API development.


1. Setup Node.js with TypeScript

# Initialize project
npm init -y

# Install TypeScript and Node types
npm install --save-dev typescript @types/node

# Initialize TypeScript
npx tsc --init

# Install ts-node for development
npm install --save-dev ts-node nodemon

tsconfig.json for Node.js

{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

package.json Scripts

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

2. Basic Express Server

npm install express
npm install --save-dev @types/express
// src/index.ts
import express, { Request, Response, NextFunction } from "express";

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

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

app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

3. Typed Request and Response

import { Request, Response } from "express";

interface User {
id: number;
name: string;
email: string;
}

// Typed request body
app.post("/users", (req: Request<{}, {}, User>, res: Response) => {
const user: User = req.body;
// Process user
res.status(201).json(user);
});

// Typed route parameters
interface UserParams {
id: string;
}

app.get("/users/:id", (req: Request<UserParams>, res: Response) => {
const userId = parseInt(req.params.id);
// Fetch user
res.json({ id: userId, name: "Alice" });
});

// Typed query parameters
interface SearchQuery {
q?: string;
page?: string;
}

app.get("/search", (req: Request<{}, {}, {}, SearchQuery>, res: Response) => {
const { q, page } = req.query;
res.json({ query: q, page: page || "1" });
});

4. Custom Request Types

import { Request } from "express";

interface AuthRequest extends Request {
user?: {
id: number;
email: string;
role: string;
};
}

// Middleware
function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
// Authentication logic
req.user = {
id: 1,
email: "user@example.com",
role: "admin"
};
next();
}

// Route handler
app.get("/profile", authenticate, (req: AuthRequest, res: Response) => {
res.json(req.user);
});

5. Middleware Types

import { RequestHandler, ErrorRequestHandler } from "express";

// Regular middleware
const logger: RequestHandler = (req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
};

// Error handling middleware
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: "Internal Server Error" });
};

app.use(logger);
app.use(errorHandler);

6. Router with Types

// routes/users.ts
import { Router, Request, Response } from "express";

const router = Router();

interface CreateUserBody {
name: string;
email: string;
}

router.get("/", (req: Request, res: Response) => {
res.json([{ id: 1, name: "Alice" }]);
});

router.post("/", (req: Request<{}, {}, CreateUserBody>, res: Response) => {
const { name, email } = req.body;
res.status(201).json({ id: 1, name, email });
});

export default router;

// app.ts
import userRoutes from "./routes/users";
app.use("/api/users", userRoutes);

7. Async/Await Error Handling

// Wrapper for async route handlers
type AsyncRequestHandler = (
req: Request,
res: Response,
next: NextFunction
) => Promise<any>;

function asyncHandler(fn: AsyncRequestHandler): RequestHandler {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}

// Usage
app.get("/users/:id", asyncHandler(async (req, res) => {
const user = await fetchUserById(req.params.id);
res.json(user);
}));

8. Service Layer Pattern

// services/userService.ts
interface User {
id: number;
name: string;
email: string;
}

class UserService {
async getUsers(): Promise<User[]> {
// Database query
return [
{ id: 1, name: "Alice", email: "alice@example.com" }
];
}

async getUserById(id: number): Promise<User | null> {
// Database query
return { id, name: "Alice", email: "alice@example.com" };
}

async createUser(data: Omit<User, "id">): Promise<User> {
// Database insert
return { id: 1, ...data };
}

async updateUser(id: number, data: Partial<User>): Promise<User | null> {
// Database update
return { id, name: "Alice", email: "alice@example.com", ...data };
}

async deleteUser(id: number): Promise<boolean> {
// Database delete
return true;
}
}

export default new UserService();

9. Controller Pattern

// controllers/userController.ts
import { Request, Response } from "express";
import userService from "../services/userService";

export class UserController {
async getAll(req: Request, res: Response): Promise<void> {
const users = await userService.getUsers();
res.json(users);
}

async getById(req: Request, res: Response): Promise<void> {
const user = await userService.getUserById(parseInt(req.params.id));
if (!user) {
res.status(404).json({ error: "User not found" });
return;
}
res.json(user);
}

async create(req: Request, res: Response): Promise<void> {
const user = await userService.createUser(req.body);
res.status(201).json(user);
}

async update(req: Request, res: Response): Promise<void> {
const user = await userService.updateUser(
parseInt(req.params.id),
req.body
);
res.json(user);
}

async delete(req: Request, res: Response): Promise<void> {
await userService.deleteUser(parseInt(req.params.id));
res.status(204).send();
}
}

10. Validation with Zod

npm install zod
import { z } from "zod";
import { Request, Response, NextFunction } from "express";

const userSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional()
});

type UserInput = z.infer<typeof userSchema>;

function validate<T>(schema: z.ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({ errors: error.errors });
} else {
next(error);
}
}
};
}

app.post("/users", validate(userSchema), (req: Request, res: Response) => {
const user: UserInput = req.body;
res.json(user);
});

11. Environment Variables

npm install dotenv
npm install --save-dev @types/dotenv
// config/env.ts
import dotenv from "dotenv";
import { z } from "zod";

dotenv.config();

const envSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
PORT: z.string().transform(Number).default("3000"),
DATABASE_URL: z.string(),
JWT_SECRET: z.string(),
API_KEY: z.string()
});

export const env = envSchema.parse(process.env);

12. Database with Prisma

npm install @prisma/client
npm install --save-dev prisma
npx prisma init
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
}

model User {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// db/client.ts
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export default prisma;

// services/userService.ts
import prisma from "../db/client";

class UserService {
async getUsers() {
return prisma.user.findMany();
}

async getUserById(id: number) {
return prisma.user.findUnique({ where: { id } });
}

async createUser(data: { name: string; email: string }) {
return prisma.user.create({ data });
}
}

13. Authentication with JWT

npm install jsonwebtoken
npm install --save-dev @types/jsonwebtoken
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";

interface JWTPayload {
userId: number;
email: string;
}

export function generateToken(payload: JWTPayload): string {
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: "7d" });
}

export function verifyToken(token: string): JWTPayload {
return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
}

// Middleware
interface AuthRequest extends Request {
user?: JWTPayload;
}

export function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
const token = req.headers.authorization?.split(" ")[1];

if (!token) {
res.status(401).json({ error: "No token provided" });
return;
}

try {
const payload = verifyToken(token);
req.user = payload;
next();
} catch (error) {
res.status(401).json({ error: "Invalid token" });
}
}

14. File Upload

npm install multer
npm install --save-dev @types/multer
import multer from "multer";

const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
}
});

const upload = multer({ storage });

app.post("/upload", upload.single("file"), (req: Request, res: Response) => {
if (!req.file) {
res.status(400).json({ error: "No file uploaded" });
return;
}
res.json({ filename: req.file.filename, path: req.file.path });
});

Key Takeaways

✅ Use @types/node and @types/express for types
✅ Type request parameters, body, and query
✅ Create custom request interfaces for middleware
✅ Use service layer for business logic
Validation with Zod for type safety
Prisma provides excellent TypeScript integration
✅ Type JWT payloads for authentication


Practice Exercises

Exercise 1: REST API

Build a complete REST API with CRUD operations for a resource.

Exercise 2: Authentication System

Implement login, register, and protected routes with JWT.

Exercise 3: File Upload Service

Create an API for uploading and managing files.


Next Steps

In Module 15, we'll explore Async/Await and Promises in depth, covering error handling, parallel execution, and advanced async patterns.