Key Takeaways
Picture this: it's Friday evening. You've just deployed what you thought was a bulletproof API update. Then the alerts start flooding in. Invalid data is slipping through validation. A missing null check is crashing the server. And somewhere in the codebase, a developer who left six months ago wrote a function that no one understands anymore.
This is the reality of building APIs with plain JavaScript. The flexibility that made Node.js popular becomes a liability when your codebase reaches a certain size. TypeScript changes that equation entirely. We've seen teams transform their API development velocity after making the switch—and you will too.
Why TypeScript with Express?
JavaScript's dynamic typing is a double-edged sword. It lets you move fast initially—but as your API grows, that speed becomes a liability. You ship a bug, users complain, and you spend hours tracing through untyped code to find the issue. TypeScript flips this narrative entirely.
The TypeScript Advantage
Compile-Time Error Detection
Catch missing properties, type mismatches, and null reference errors before deployment. No more debugging undefined is not a function at 3 AM.
Self-Documenting Code
Interfaces and type definitions serve as living documentation. New team members understand data structures without reading through implementation details.
Confident Refactoring
Rename a function or change a return type—TypeScript tells you exactly what breaks. No more fear of making changes to legacy code.
Enhanced IDE Support
Autocomplete, inline documentation, and intelligent suggestions transform your development experience. Write code faster with fewer mistakes.
At Boundev, we've built APIs for companies ranging from early-stage startups to enterprise organizations. The pattern is consistent: teams that adopt TypeScript early spend 40% less time on bug fixes and ship features faster. The initial learning curve pays for itself within the first month.
Struggling to find experienced Node.js developers?
Boundev's staff augmentation model places pre-vetted TypeScript developers in your team within 72 hours—no months of interviewing.
See How We Do ItSetting Up Your Project
Let's build this properly from the ground up. First, initialize your project and install the necessary dependencies. We're going to create a structure that scales—you can thank us later when your API grows from five endpoints to fifty.
mkdir express-typescript-api
cd express-typescript-api
npm init -y
npm install express cors helmet morgan
npm install -D typescript ts-node @types/node @types/express @types/cors @types/morgan nodemon
Now let's configure TypeScript for optimal Express.js development:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
The strict: true flag is non-negotiable. It enables every strict type-checking option, catching potential issues at compile time. This is the single most important setting in your TypeScript configuration.
Building the Application Structure
A scalable API needs a clear structure. We're going to organize our code into logical layers: routes for handling HTTP requests, controllers for business logic, and services for data operations. This separation makes testing easier and keeps your code maintainable.
Project Structure
Let's start with defining our data models using TypeScript interfaces. This is where the magic happens—type safety from the ground up:
export interface User {
id: string;
email: string;
name: string;
createdAt: Date;
updatedAt: Date;
}
export interface CreateUserDTO {
email: string;
name: string;
password: string;
}
export interface UpdateUserDTO {
email?: string;
name?: string;
}
export interface UserResponse extends Omit {}
Notice how we're using TypeScript's utility types like Omit to automatically create our response type. This prevents accidentally leaking sensitive fields like passwords to API responses. These small patterns add up to create a robust, secure API.
Creating Express Routes and Controllers
Routes define your API's contract with the outside world. Let's create clean, RESTful endpoints with proper TypeScript typing throughout:
import { Router, Request, Response, NextFunction } from 'express';
import { UserController } from '../controllers/user.controller';
const router = Router();
const userController = new UserController();
router.post('/users', (req: Request, res: Response, next: NextFunction) =>
userController.createUser(req, res, next));
router.get('/users', (req: Request, res: Response, next: NextFunction) =>
userController.getUsers(req, res, next));
router.get('/users/:id', (req: Request, res: Response, next: NextFunction) =>
userController.getUserById(req, res, next));
router.put('/users/:id', (req: Request, res: Response, next: NextFunction) =>
userController.updateUser(req, res, next));
router.delete('/users/:id', (req: Request, res: Response, next: NextFunction) =>
userController.deleteUser(req, res, next));
export default router;
We're using the Request and Response types from Express, along with NextFunction for error handling. TypeScript ensures our route handlers have the correct signatures—no more guessing about what parameters your handlers accept.
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user.service';
import { CreateUserDTO, UpdateUserDTO } from '../models/user';
export class UserController {
private userService: UserService;
constructor() {
this.userService = new UserService();
}
async createUser(req: Request, res: Response, next: NextFunction) {
try {
const userData: CreateUserDTO = req.body;
const user = await this.userService.create(userData);
res.status(201).json(user);
} catch (error) {
next(error);
}
}
async getUsers(req: Request, res: Response, next: NextFunction) {
try {
const users = await this.userService.findAll();
res.status(200).json(users);
} catch (error) {
next(error);
}
}
async getUserById(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const user = await this.userService.findById(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(200).json(user);
} catch (error) {
next(error);
}
}
async updateUser(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const userData: UpdateUserDTO = req.body;
const user = await this.userService.update(id, userData);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(200).json(user);
} catch (error) {
next(error);
}
}
async deleteUser(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
await this.userService.delete(id);
res.status(204).send();
} catch (error) {
next(error);
}
}
}
This is where TypeScript truly shines. Notice how we're explicitly typing our request body as CreateUserDTO and UpdateUserDTO. If someone tries to pass an invalid field or forgets a required property, TypeScript will flag it at compile time—long before it reaches your API.
Ready to Build Your Remote Team?
Partner with Boundev to access pre-vetted developers experienced in building production-ready APIs with Node.js and TypeScript.
Talk to Our TeamMiddleware: The Backbone of Your API
Middleware functions are the secret weapon of well-architected Express APIs. They let you run code on every request, modify request/response objects, and handle errors consistently. Let's create some essential middleware:
import { Request, Response, NextFunction, RequestHandler } from 'express';
import { ZodSchema } from 'zod';
export const validateRequest = (schema: ZodSchema): RequestHandler => {
return (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
return res.status(400).json({
error: 'Validation failed',
details: error
});
}
};
};
This middleware uses Zod for runtime validation, but because we're using TypeScript, our IDE autocomplete works with the schema definitions. You get compile-time safety AND runtime validation—a powerful combination.
Essential Middleware Stack
Error Handling
Centralized error handling that catches exceptions and returns consistent error responses.
Request Logging
Morgan or Winston for detailed request logging—essential for debugging production issues.
Security Headers
Helmet middleware adds essential security headers (HSTS, X-Frame-Options, CSP).
CORS Configuration
Proper CORS setup to control which domains can access your API.
Rate Limiting
Protect your API from abuse with express-rate-limit.
The App Entry Point
Now let's bring it all together in our main application file. This is where we configure Express and wire up all our middleware and routes:
import express, { Application, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import userRoutes from './routes/user.routes';
const app: Application = express();
// Security & Middleware
app.use(helmet());
app.use(cors());
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api', userRoutes);
// Health check
app.get('/health', (req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Global error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error('Error:', err);
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
export default app;
The global error handler at the end is crucial. It catches any errors that slip through your route handlers and returns a consistent error response. In production, you'd hide the error message but log it for debugging. If you're building APIs at this level of quality, you're probably thinking about scaling your team too.
How Boundev Solves This for You
Everything we've covered in this blog—TypeScript interfaces, middleware patterns, proper error handling—is exactly what our team handles every day when building APIs for clients. Here's how we approach it for our partners.
We build you a full remote engineering team—screened, onboarded, and shipping production-ready APIs in under a week.
Plug pre-vetted TypeScript developers directly into your existing team—no re-training, no culture mismatch, no delays.
Hand us the entire API project. We manage architecture, development, testing, and deployment—you focus on the business.
The Bottom Line
Building APIs with TypeScript and Express isn't just about following trends—it's about building software that scales, is maintainable, and doesn't break at 2 AM. The type safety, IDE support, and code documentation pay dividends as your codebase grows.
Need to scale your API development?
Boundev places pre-vetted Node.js and TypeScript developers who already understand these patterns—no training required.
See How We Do ItFrequently Asked Questions
Why should I use TypeScript instead of JavaScript for my Express API?
TypeScript catches errors at compile time rather than runtime, provides excellent IDE autocomplete for faster development, creates self-documenting code that makes onboarding easier, and enables confident refactoring. Teams using TypeScript typically spend 40% less time on bug fixes. The initial setup cost pays for itself within weeks.
What's the best project structure for a TypeScript Express API?
Organize by functionality with separate folders for routes, controllers, services, models, middleware, and config. This separation of concerns makes testing easier and keeps code maintainable as your API grows. Avoid putting everything in one file—future you will thank present you.
How do I handle validation in a TypeScript Express API?
Use Zod or Yup for runtime validation combined with TypeScript's compile-time checking. Create middleware that validates incoming requests against your schemas. This gives you double protection—TypeScript catches obvious errors during development, while Zod validates at runtime against malicious or malformed input.
How do I deploy a TypeScript Express API to production?
Compile your TypeScript to JavaScript using `tsc`, then deploy the compiled output. Use PM2 or a container service like Docker. Set NODE_ENV to production, enable proper logging, and configure health check endpoints. Most teams use CI/CD pipelines (GitHub Actions, GitLab CI) that compile and test automatically before deploying.
Can I mix JavaScript and TypeScript in my Express project?
Yes, but it's not recommended. TypeScript can compile JavaScript files, but you lose type safety on those files. It's better to fully commit to TypeScript from the start. If you're migrating an existing JavaScript project, convert files incrementally—the TypeScript compiler will help you identify issues.
Explore Boundev's Services
Ready to put what you just learned into action? Here's how we can help.
Build the engineering team behind scalable Node.js APIs with pre-vetted developers.
Learn more →
Scale your existing team with experienced TypeScript developers in under 72 hours.
Learn more →
Outsource your API development to experts who build production-ready systems.
Learn more →
Let's Build This Together
You now know exactly what it takes to build production-ready APIs with TypeScript and Express. The next step is execution—and that's where Boundev comes in.
200+ companies have trusted us to build their engineering teams. Tell us what you need—we'll respond within 24 hours.
