TypeScript and Node.js
TypeScript brings the power of static typing to Node.js development, helping you build more reliable and maintainable server-side applications. By catching errors during development rather than in production, TypeScript significantly improves the quality of your Node.js code while maintaining all the flexibility JavaScript is known for. Whether you're building APIs, microservices, or full-stack applications, combining TypeScript with Node.js creates a robust development environment.
Setting Up TypeScript with Node.js
Getting started with TypeScript in a Node.js project is straightforward. Here's how to set up a new project:
1. Initialize your project:
npm init -y
2. Install TypeScript and required dependencies:
npm install typescript ts-node @types/node --save-dev
3. Create a tsconfig.json file:
npx tsc --init
4. Configure tsconfig.json for Node.js:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
5. Add development scripts to package.json:
"scripts": {
"dev": "ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
For existing Node.js projects, you can gradually migrate to TypeScript by:
- 1Renaming
.js
files to.ts
one at a time - 2Adding type annotations incrementally
- 3Using JSDoc comments for type hints during transition
- 4Configuring
allowJs
in tsconfig.json to mix JavaScript and TypeScript
Writing Server-Side Code with TypeScript
TypeScript enhances Node.js development by providing type safety for your server-side code. Here are key patterns and examples:
Basic Server with Express
import express, { Application, Request, Response } from 'express';
const app: Application = express();
const PORT: number = 3000;
app.get('/', (req: Request, res: Response) => {
res.send('Hello TypeScript with Node.js!');
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Typed Database Models
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
class UserRepository {
private users: User[] = [];
addUser(user: Omit<User, 'id' | 'createdAt'>): User {
const newUser: User = {
...user,
id: Math.random().toString(36).substring(2),
createdAt: new Date()
};
this.users.push(newUser);
return newUser;
}
getUserById(id: string): User | undefined {
return this.users.find(user => user.id === id);
}
}
Error Handling with Custom Error Types
class AppError extends Error {
constructor(
public message: string,
public statusCode: number = 500,
public details?: any
) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
// Usage
app.get('/user/:id', (req: Request, res: Response) => {
const user = userRepository.getUserById(req.params.id);
if (!user) {
throw new AppError('User not found', 404);
}
res.json(user);
});
// Error handling middleware
app.use((err: Error, req: Request, res: Response) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: err.message,
details: err.details
});
}
res.status(500).json({ error: 'Internal Server Error' });
});
TypeScript for API Development
TypeScript shines when building APIs, providing type safety for both request/response payloads and database operations.
REST API with Typed Routes
import { Router } from 'express';
interface Product {
id: string;
name: string;
price: number;
inStock: boolean;
}
const products: Product[] = [
{ id: '1', name: 'Laptop', price: 999, inStock: true },
{ id: '2', name: 'Mouse', price: 25, inStock: false }
];
const router = Router();
// GET /products
router.get('/', (req, res) => {
res.json(products);
});
// POST /products
router.post('/', (req: Request<{}, {}, Omit<Product, 'id'>>, res) => {
const newProduct: Product = {
...req.body,
id: Math.random().toString(36).substring(2)
};
products.push(newProduct);
res.status(201).json(newProduct);
});
// GET /products/:id
router.get('/:id', (req: Request<{ id: string }>, res) => {
const product = products.find(p => p.id === req.params.id);
if (!product) return res.status(404).json({ error: 'Not found' });
res.json(product);
});
GraphQL API with Type Safety
import { ApolloServer } from 'apollo-server';
import { gql } from 'graphql-tag';
interface Book {
id: string;
title: string;
author: string;
}
const books: Book[] = [
{ id: '1', title: 'TypeScript Basics', author: 'John Doe' },
{ id: '2', title: 'Advanced Node.js', author: 'Jane Smith' }
];
const typeDefs = gql`
type Book {
id: ID!
title: String!
author: String!
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
`;
const resolvers = {
Query: {
books: (): Book[] => books,
book: (_: any, args: { id: string }): Book | undefined =>
books.find(book => book.id === args.id)
}
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
WebSocket API with Type Safety
import { WebSocketServer } from 'ws';
interface Message {
type: 'chat' | 'notification';
content: string;
timestamp: Date;
userId: string;
}
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', ws => {
ws.on('message', (data: string) => {
try {
const message: Message = JSON.parse(data);
// Validate message structure
if (!message.type || !message.content) {
throw new Error('Invalid message format');
}
// Broadcast to all clients
wss.clients.forEach(client => {
client.send(JSON.stringify({
...message,
timestamp: new Date()
}));
});
} catch (err) {
ws.send(JSON.stringify({ error: 'Invalid message' }));
}
});
});
Advanced Patterns
1. Repository Pattern with Generics:
interface Repository<T> {
getAll(): Promise<T[]>;
getById(id: string): Promise<T | null>;
create(item: Omit<T, 'id'>): Promise<T>;
update(id: string, item: Partial<T>): Promise<T | null>;
delete(id: string): Promise<boolean>;
}
class MongoUserRepository implements Repository<User> {
// Implementation using MongoDB driver
}
2. Middleware Typing:
interface AuthenticatedRequest extends Request {
user: {
id: string;
role: 'admin' | 'user';
};
}
const authMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
// Authentication logic
const authenticatedReq = req as AuthenticatedRequest;
authenticatedReq.user = { id: '123', role: 'user' };
next();
};
app.get('/profile', authMiddleware, (req: AuthenticatedRequest, res) => {
res.json({ user: req.user });
});
3. Configuration Typing:
interface AppConfig {
port: number;
db: {
host: string;
port: number;
name: string;
};
auth: {
secret: string;
expiresIn: string;
};
}
const config: AppConfig = {
port: parseInt(process.env.PORT || '3000'),
db: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '27017'),
name: process.env.DB_NAME || 'mydb'
},
auth: {
secret: process.env.AUTH_SECRET || 'defaultsecret',
expiresIn: process.env.AUTH_EXPIRES || '1h'
}
};
By leveraging TypeScript in your Node.js projects, you'll benefit from:
- Early detection of potential runtime errors
- Better code documentation through types
- Improved developer experience with autocompletion
- Easier refactoring of large codebases
- More reliable API contracts between frontend and backend
The combination of Node.js and TypeScript creates a powerful environment for building scalable, maintainable server-side applications that stand the test of time.