15 November 2024
Upida
6 menit baca
Artikel Unggulan
#TypeScript #Express #API #Backend #Node.js

Membangun API dengan TypeScript dan Express: Best Practices

Panduan lengkap membangun REST API yang robust dengan TypeScript, Express, dan implementasi best practices untuk production-ready applications.

Bagikan artikel:

Membangun API dengan TypeScript dan Express: Best Practices

Membangun API yang robust dan maintainable memerlukan perencanaan yang matang dan implementasi best practices. Dalam artikel ini, kita akan membahas bagaimana membangun REST API menggunakan TypeScript dan Express dengan standar production-ready.

Setup Project

Inisialisasi Project

mkdir api-typescript-express
cd api-typescript-express
npm init -y

Dependencies yang Dibutuhkan

# Production dependencies
npm install express cors helmet morgan compression
npm install jsonwebtoken bcryptjs joi
npm install prisma @prisma/client

# Development dependencies
npm install -D typescript @types/node @types/express
npm install -D @types/cors @types/helmet @types/morgan
npm install -D @types/jsonwebtoken @types/bcryptjs
npm install -D nodemon ts-node

TypeScript Configuration

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

Struktur Project yang Scalable

src/
├── controllers/
├── middlewares/
├── models/
├── routes/
├── services/
├── types/
├── utils/
├── config/
└── app.ts

Implementasi Type-Safe API

Definisi Types

// src/types/user.types.ts
export interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface CreateUserRequest {
  email: string;
  name: string;
  password: string;
}

export interface LoginRequest {
  email: string;
  password: string;
}

export interface AuthResponse {
  user: Omit<User, 'password'>;
  token: string;
}

Request Validation dengan Joi

// src/utils/validation.ts
import Joi from 'joi';

export const userSchema = {
  create: Joi.object({
    email: Joi.string().email().required(),
    name: Joi.string().min(2).max(50).required(),
    password: Joi.string().min(8).required()
  }),
  
  login: Joi.object({
    email: Joi.string().email().required(),
    password: Joi.string().required()
  })
};

export const validateRequest = (schema: Joi.Schema) => {
  return (req: Request, res: Response, next: NextFunction) => {
    const { error } = schema.validate(req.body);
    if (error) {
      return res.status(400).json({
        success: false,
        message: error.details[0].message
      });
    }
    next();
  };
};

Error Handling Middleware

// src/middlewares/errorHandler.ts
import { Request, Response, NextFunction } from 'express';

export class AppError extends Error {
  statusCode: number;
  isOperational: boolean;

  constructor(message: string, statusCode: number) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}

export const errorHandler = (
  err: AppError,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const { statusCode = 500, message } = err;

  res.status(statusCode).json({
    success: false,
    message: statusCode === 500 ? 'Internal Server Error' : message,
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
};

Authentication Middleware

// src/middlewares/auth.ts
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { AppError } from './errorHandler';

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

export const authenticate = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const token = req.header('Authorization')?.replace('Bearer ', '');
    
    if (!token) {
      throw new AppError('Access token required', 401);
    }

    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
    req.user = decoded;
    next();
  } catch (error) {
    next(new AppError('Invalid token', 401));
  }
};

Service Layer Pattern

// src/services/user.service.ts
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { prisma } from '../config/database';
import { CreateUserRequest, LoginRequest, AuthResponse } from '../types/user.types';
import { AppError } from '../middlewares/errorHandler';

export class UserService {
  static async createUser(userData: CreateUserRequest): Promise<AuthResponse> {
    const existingUser = await prisma.user.findUnique({
      where: { email: userData.email }
    });

    if (existingUser) {
      throw new AppError('User already exists', 400);
    }

    const hashedPassword = await bcrypt.hash(userData.password, 12);
    
    const user = await prisma.user.create({
      data: {
        ...userData,
        password: hashedPassword
      }
    });

    const token = this.generateToken(user.id, user.email);

    return {
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        createdAt: user.createdAt,
        updatedAt: user.updatedAt
      },
      token
    };
  }

  static async loginUser(loginData: LoginRequest): Promise<AuthResponse> {
    const user = await prisma.user.findUnique({
      where: { email: loginData.email }
    });

    if (!user || !await bcrypt.compare(loginData.password, user.password)) {
      throw new AppError('Invalid credentials', 401);
    }

    const token = this.generateToken(user.id, user.email);

    return {
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        createdAt: user.createdAt,
        updatedDate: user.updatedAt
      },
      token
    };
  }

  private static generateToken(userId: string, email: string): string {
    return jwt.sign(
      { userId, email },
      process.env.JWT_SECRET!,
      { expiresIn: '7d' }
    );
  }
}

Controller Implementation

// src/controllers/user.controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user.service';
import { CreateUserRequest, LoginRequest } from '../types/user.types';

export class UserController {
  static async register(req: Request, res: Response, next: NextFunction) {
    try {
      const userData: CreateUserRequest = req.body;
      const result = await UserService.createUser(userData);
      
      res.status(201).json({
        success: true,
        data: result
      });
    } catch (error) {
      next(error);
    }
  }

  static async login(req: Request, res: Response, next: NextFunction) {
    try {
      const loginData: LoginRequest = req.body;
      const result = await UserService.loginUser(loginData);
      
      res.json({
        success: true,
        data: result
      });
    } catch (error) {
      next(error);
    }
  }

  static async getProfile(req: Request, res: Response, next: NextFunction) {
    try {
      const userId = req.user?.userId;
      const user = await UserService.getUserById(userId);
      
      res.json({
        success: true,
        data: { user }
      });
    } catch (error) {
      next(error);
    }
  }
}

API Routes

// src/routes/user.routes.ts
import { Router } from 'express';
import { UserController } from '../controllers/user.controller';
import { validateRequest } from '../utils/validation';
import { authenticate } from '../middlewares/auth';
import { userSchema } from '../utils/validation';

const router = Router();

router.post('/register', 
  validateRequest(userSchema.create),
  UserController.register
);

router.post('/login',
  validateRequest(userSchema.login),
  UserController.login
);

router.get('/profile',
  authenticate,
  UserController.getProfile
);

export default router;

Security Best Practices

Rate Limiting

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP'
});

app.use('/api/', limiter);

Security Headers

import helmet from 'helmet';
import cors from 'cors';

app.use(helmet());
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  credentials: true
}));

Environment Configuration

// src/config/env.ts
import dotenv from 'dotenv';

dotenv.config();

export const config = {
  port: process.env.PORT || 3000,
  jwtSecret: process.env.JWT_SECRET!,
  databaseUrl: process.env.DATABASE_URL!,
  nodeEnv: process.env.NODE_ENV || 'development'
};

// Validate required environment variables
const requiredEnvVars = ['JWT_SECRET', 'DATABASE_URL'];
const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar]);

if (missingEnvVars.length > 0) {
  throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);
}

Kesimpulan

Membangun API dengan TypeScript dan Express memerlukan perhatian pada beberapa aspek penting:

  1. Type Safety - Gunakan TypeScript untuk mencegah runtime errors
  2. Validation - Validasi input dengan library seperti Joi
  3. Error Handling - Implementasi centralized error handling
  4. Security - Terapkan security best practices
  5. Architecture - Gunakan layered architecture untuk maintainability

Dengan mengikuti best practices ini, Anda dapat membangun API yang robust, scalable, dan production-ready.


Artikel ini merupakan panduan dasar yang dapat dikembangkan lebih lanjut sesuai kebutuhan project Anda. Happy coding!

U

Upida

Full Stack Developer & Tech Enthusiast. Passionate dalam mengembangkan solusi teknologi yang inovatif dan bermanfaat.