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.