DEV Community

Cover image for Getting Started with TypeScript Backend Development: Building Scalable and Maintainable Server-Side Applications
Dmytro Shatokhin
Dmytro Shatokhin

Posted on

Getting Started with TypeScript Backend Development: Building Scalable and Maintainable Server-Side Applications

Introduction

TypeScript has rapidly become a cornerstone of modern backend development, combining the power of static typing with the flexibility of JavaScript. Its ability to catch errors at compile time, improve code maintainability, and scale large applications makes it an ideal choice for backend systems. Paired with frameworks like Node.js and Express, TypeScript enables developers to build robust, type-safe APIs and microservices.

This article walks you through the essentials of setting up a TypeScript backend, from environment configuration to deploying a production-ready application. Whether you're transitioning from JavaScript or starting fresh, this guide provides actionable steps to kickstart your project.


Setting Up the Development Environment

Before diving into code, ensure your machine has the necessary tools:

  1. Node.js and npm: Install the latest LTS version from nodejs.org.
  2. TypeScript: Install globally via npm:
   npm install -g typescript  
Enter fullscreen mode Exit fullscreen mode
  1. ts-node: Enables running TypeScript directly without precompiling:
   npm install -g ts-node  
Enter fullscreen mode Exit fullscreen mode
  1. Type Definitions: Install Node.js type definitions for autocompletion and linting:
   npm install --save-dev @types/node  
Enter fullscreen mode Exit fullscreen mode

Initialize a Project

mkdir my-ts-backend  
cd my-ts-backend  
npm init -y  
npm install express  
npm install --save-dev typescript ts-node @types/express  
Enter fullscreen mode Exit fullscreen mode

Create a tsconfig.json file to configure TypeScript:

npx tsc --init  
Enter fullscreen mode Exit fullscreen mode

Edit tsconfig.json to match backend needs:

{  
  "target": "ES2020",  
  "module": "CommonJS",  
  "outDir": "./dist",  
  "strict": true,  
  "esModuleInterop": true,  
  "skipLibCheck": true  
}  
Enter fullscreen mode Exit fullscreen mode

Project Structure

A well-organized structure ensures scalability. Adopt this common layout:

src/  
├── controllers/      # Handle HTTP requests  
├── services/         # Business logic  
├── routes/           # API endpoint mappings  
├── middleware/       # Custom middleware (e.g., auth)  
├── models/           # Database models  
├── config/           # Configuration files  
├── utils/            # Helper functions  
├── index.ts          # Entry point  
.env                  # Environment variables  
package.json  
tsconfig.json  
Enter fullscreen mode Exit fullscreen mode

Creating a Basic Server with Express

Start with a minimal Express server in src/index.ts:

import express, { Express, Request, Response } from 'express';  

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

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

app.listen(PORT, () => {  
  console.log(`Server running at http://localhost:${PORT}`);  
});  
Enter fullscreen mode Exit fullscreen mode

Run the server with:

npx ts-node src/index.ts  
Enter fullscreen mode Exit fullscreen mode

Implementing Business Logic and Middleware

Controllers and Services

Separate concerns by splitting request handling (controllers) from business logic (services).

Example Controller (src/controllers/userController.ts):

import { Request, Response } from 'express';  
import { UserService } from '../services/userService';  

export class UserController {  
  private userService: UserService;  

  constructor() {  
    this.userService = new UserService();  
  }  

  public getAllUsers = (req: Request, res: Response) => {  
    const users = this.userService.fetchAll();  
    res.status(200).json(users);  
  };  
}  
Enter fullscreen mode Exit fullscreen mode

Service (src/services/userService.ts):

export class UserService {  
  public fetchAll(): string[] {  
    return ['Alice', 'Bob']; // Mock data  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

Middleware

Create reusable middleware in src/middleware/loggingMiddleware.ts:

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

export const logger = (req: Request, res: Response, next: NextFunction) => {  
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);  
  next();  
};  
Enter fullscreen mode Exit fullscreen mode

Apply it in index.ts:

app.use(logger);  
Enter fullscreen mode Exit fullscreen mode

Connecting to a Database

TypeScript works seamlessly with ORMs like TypeORM or Mongoose. Here's a Mongoose example:

  1. Install dependencies:
   npm install mongoose  
   npm install --save-dev @types/mongoose  
Enter fullscreen mode Exit fullscreen mode
  1. Define a Model (src/models/User.ts):
   import { model, Schema } from 'mongoose';  

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

   const UserSchema = new Schema<User>({  
     name: { type: String, required: true },  
     email: { type: String, required: true, unique: true }  
   });  

   export default model<User>('User', UserSchema);  
Enter fullscreen mode Exit fullscreen mode
  1. Connect in index.ts:
   import mongoose from 'mongoose';  
   mongoose.connect('mongodb://localhost:27017/mydb');  
Enter fullscreen mode Exit fullscreen mode

Environment Variables and Configuration

Use dotenv to manage environment variables:

  1. Install:
   npm install dotenv  
Enter fullscreen mode Exit fullscreen mode
  1. Create .env:
   PORT=4000  
   DB_URI=mongodb://localhost:27017/mydb  
Enter fullscreen mode Exit fullscreen mode
  1. Access in src/config/index.ts:
   import dotenv from 'dotenv';  
   dotenv.config();  

   export default {  
     port: process.env.PORT,  
     dbUri: process.env.DB_URI  
   };  
Enter fullscreen mode Exit fullscreen mode

Error Handling and Validation

Centralized Error Handling

Create an error middleware (src/middleware/errorMiddleware.ts):

import { NextFunction, Request, Response } from 'express';  
import { AppError } from '../utils/appError';  

export const errorHandler = (err: AppError, req: Request, res: Response, next: NextFunction) => {  
  const statusCode = err.statusCode || 500;  
  res.status(statusCode).json({  
    status: 'error',  
    message: err.message  
  });  
};  
Enter fullscreen mode Exit fullscreen mode

Input Validation

Use zod for schema validation:

npm install zod  
Enter fullscreen mode Exit fullscreen mode

Example validation pipe (src/utils/validate.ts):

import { ZodSchema } from 'zod';  

export const validate = (schema: ZodSchema) => (payload: unknown) => {  
  return schema.parse(payload);  
};  
Enter fullscreen mode Exit fullscreen mode

Testing the Backend

Use Jest and Supertest for testing:

  1. Install:
   npm install --save-dev jest supertest  
Enter fullscreen mode Exit fullscreen mode
  1. Write a test (__tests__/index.test.ts):
   import request from 'supertest';  
   import app from '../src/index';  

   describe('GET /', () => {  
     it('returns a 200 status code', async () => {  
       const response = await request(app).get('/');  
       expect(response.status).toBe(200);  
     });  
   });  
Enter fullscreen mode Exit fullscreen mode

Run tests:

npm test  
Enter fullscreen mode Exit fullscreen mode

Deployment Considerations

  1. Compile TypeScript:
   npx tsc  
Enter fullscreen mode Exit fullscreen mode
  1. Production Build:

    The compiled JS files will be in /dist.

  2. Dockerize:

   FROM node:18-alpine  
   WORKDIR /app  
   COPY package*.json ./  
   RUN npm ci --only=production  
   COPY dist/ .  
   CMD ["node", "index.js"]  
Enter fullscreen mode Exit fullscreen mode
  1. Deploy: Use platforms like Vercel, Heroku, or AWS with CI/CD pipelines.

Conclusion

TypeScript transforms backend development by enforcing type safety and reducing runtime errors. By following this guide, you've set up a scalable project structure, integrated Express and a database, and implemented best practices for error handling and testing.

Next steps:

  • Explore advanced TypeScript features (generics, decorators).
  • Implement authentication (JWT, OAuth).
  • Monitor performance with tools like Winston or Datadog.

With this foundation, you're equipped to build enterprise-grade backends that stand the test of time.

Top comments (0)