Back to Blogs

Mastering Modern Backend Development: From APIs to Microservices

Dive deep into modern backend development practices, exploring RESTful APIs, GraphQL, microservices architecture, and cloud-native development patterns that power today's applications.

December 5, 2024
15 min read
1,234 views
Jordan Wilfry
Jordan Wilfry
BackendAPIsMicroservicesNode.jsGraphQLDevOpsCloud
Mastering Modern Backend Development: From APIs to Microservices

Mastering Modern Backend Development: From APIs to Microservices

Backend development has evolved significantly over the past few years. Modern applications require robust, scalable, and maintainable backend systems that can handle millions of users while remaining flexible enough to adapt to changing business requirements.

1. API Design and Development

RESTful API Best Practices

Creating well-designed APIs is crucial for modern applications:

// Express.js with TypeScript
import express from 'express'
import { body, validationResult } from 'express-validator'

const app = express()

// Middleware for validation
const validateUser = [
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }),
  body('name').trim().isLength({ min: 2 })
]

// RESTful endpoints
app.post('/api/users', validateUser, async (req, res) => {
  const errors = validationResult(req)
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() })
  }

  try {
    const user = await userService.createUser(req.body)
    res.status(201).json({ data: user, message: 'User created successfully' })
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' })
  }
})

app.get('/api/users/:id', async (req, res) => {
  try {
    const user = await userService.getUserById(req.params.id)
    if (!user) {
      return res.status(404).json({ error: 'User not found' })
    }
    res.json({ data: user })
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' })
  }
})

GraphQL for Flexible Data Fetching

GraphQL provides a more flexible alternative to REST:

import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'

const typeDefs = `
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    createPost(input: CreatePostInput!): Post!
  }

  input CreateUserInput {
    name: String!
    email: String!
  }

  input CreatePostInput {
    title: String!
    content: String!
    authorId: ID!
  }
`

const resolvers = {
  Query: {
    users: () => userService.getAllUsers(),
    user: (_, { id }) => userService.getUserById(id),
    posts: () => postService.getAllPosts(),
  },
  Mutation: {
    createUser: (_, { input }) => userService.createUser(input),
    createPost: (_, { input }) => postService.createPost(input),
  },
  User: {
    posts: (user) => postService.getPostsByAuthorId(user.id),
  },
  Post: {
    author: (post) => userService.getUserById(post.authorId),
  },
}

const server = new ApolloServer({ typeDefs, resolvers })

2. Database Design and Optimization

Modern Database Patterns

Repository Pattern with TypeORM

import { Entity, PrimaryGeneratedColumn, Column, Repository } from 'typeorm'

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ unique: true })
  email: string

  @Column()
  name: string

  @Column()
  passwordHash: string

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date
}

export class UserRepository {
  constructor(private repository: Repository<User>) {}

  async findById(id: string): Promise<User | null> {
    return this.repository.findOne({ where: { id } })
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.repository.findOne({ where: { email } })
  }

  async create(userData: Partial<User>): Promise<User> {
    const user = this.repository.create(userData)
    return this.repository.save(user)
  }

  async update(id: string, userData: Partial<User>): Promise<User | null> {
    await this.repository.update(id, userData)
    return this.findById(id)
  }
}

Database Optimization Techniques

-- Indexing for performance
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_author_created ON posts(author_id, created_at);

-- Composite indexes for complex queries
CREATE INDEX idx_posts_status_category ON posts(status, category_id) 
WHERE status = 'published';

-- Partial indexes for specific conditions
CREATE INDEX idx_active_users ON users(last_login) 
WHERE status = 'active';

3. Microservices Architecture

Service Communication Patterns

Event-Driven Architecture with Message Queues

import amqp from 'amqplib'

class EventBus {
  private connection: amqp.Connection
  private channel: amqp.Channel

  async connect() {
    this.connection = await amqp.connect('amqp://localhost')
    this.channel = await this.connection.createChannel()
  }

  async publish(exchange: string, routingKey: string, data: any) {
    await this.channel.assertExchange(exchange, 'topic', { durable: true })
    
    const message = Buffer.from(JSON.stringify(data))
    this.channel.publish(exchange, routingKey, message, { persistent: true })
  }

  async subscribe(exchange: string, routingKey: string, handler: (data: any) => void) {
    await this.channel.assertExchange(exchange, 'topic', { durable: true })
    
    const queue = await this.channel.assertQueue('', { exclusive: true })
    await this.channel.bindQueue(queue.queue, exchange, routingKey)
    
    this.channel.consume(queue.queue, (msg) => {
      if (msg) {
        const data = JSON.parse(msg.content.toString())
        handler(data)
        this.channel.ack(msg)
      }
    })
  }
}

// Usage in User Service
class UserService {
  constructor(private eventBus: EventBus) {}

  async createUser(userData: CreateUserInput) {
    const user = await this.userRepository.create(userData)
    
    // Publish event for other services
    await this.eventBus.publish('user.events', 'user.created', {
      userId: user.id,
      email: user.email,
      timestamp: new Date()
    })
    
    return user
  }
}

// Usage in Email Service
class EmailService {
  constructor(private eventBus: EventBus) {
    this.setupEventHandlers()
  }

  private setupEventHandlers() {
    this.eventBus.subscribe('user.events', 'user.created', this.handleUserCreated.bind(this))
  }

  private async handleUserCreated(event: { userId: string, email: string }) {
    await this.sendWelcomeEmail(event.email)
  }
}

API Gateway Pattern

import express from 'express'
import httpProxy from 'http-proxy-middleware'
import rateLimit from 'express-rate-limit'

const app = express()

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
})

app.use(limiter)

// Authentication middleware
const authenticate = async (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '')
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' })
  }

  try {
    const user = await authService.verifyToken(token)
    req.user = user
    next()
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' })
  }
}

// Service proxies
app.use('/api/users', authenticate, httpProxy({
  target: 'http://user-service:3001',
  changeOrigin: true,
  pathRewrite: { '^/api/users': '' }
}))

app.use('/api/posts', authenticate, httpProxy({
  target: 'http://post-service:3002',
  changeOrigin: true,
  pathRewrite: { '^/api/posts': '' }
}))

app.use('/api/notifications', authenticate, httpProxy({
  target: 'http://notification-service:3003',
  changeOrigin: true,
  pathRewrite: { '^/api/notifications': '' }
}))

4. Cloud-Native Development

Containerization with Docker

# Multi-stage build for Node.js application
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS runtime

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

WORKDIR /app

# Copy built application
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --chown=nextjs:nodejs . .

USER nextjs

EXPOSE 3000

CMD ["npm", "start"]

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: user-service:latest
        ports:
        - containerPort: 3000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
  - port: 80
    targetPort: 3000
  type: ClusterIP

5. Monitoring and Observability

Structured Logging

import winston from 'winston'

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'user-service' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
    new winston.transports.Console({
      format: winston.format.simple()
    })
  ]
})

// Usage in application
app.use((req, res, next) => {
  logger.info('Request received', {
    method: req.method,
    url: req.url,
    userAgent: req.get('User-Agent'),
    ip: req.ip
  })
  next()
})

Health Checks and Metrics

import express from 'express'
import prometheus from 'prom-client'

const app = express()

// Prometheus metrics
const httpRequestDuration = new prometheus.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code']
})

const httpRequestTotal = new prometheus.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code']
})

// Metrics middleware
app.use((req, res, next) => {
  const start = Date.now()
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000
    const labels = {
      method: req.method,
      route: req.route?.path || req.path,
      status_code: res.statusCode
    }
    
    httpRequestDuration.observe(labels, duration)
    httpRequestTotal.inc(labels)
  })
  
  next()
})

// Health check endpoint
app.get('/health', async (req, res) => {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    checks: {
      database: await checkDatabase(),
      redis: await checkRedis(),
      externalApi: await checkExternalApi()
    }
  }
  
  const isHealthy = Object.values(health.checks).every(check => check.status === 'healthy')
  
  res.status(isHealthy ? 200 : 503).json(health)
})

// Metrics endpoint
app.get('/metrics', (req, res) => {
  res.set('Content-Type', prometheus.register.contentType)
  res.end(prometheus.register.metrics())
})

Conclusion

Modern backend development requires a comprehensive understanding of various technologies and patterns. From designing robust APIs to implementing microservices architecture and ensuring proper monitoring, each aspect plays a crucial role in building scalable and maintainable systems.

The key to success is choosing the right tools and patterns for your specific use case, while always keeping scalability, maintainability, and performance in mind. Start with simple solutions and evolve your architecture as your application grows.

Remember, the best architecture is one that serves your current needs while providing a clear path for future growth and evolution.

Jordan Wilfry

About Jordan Wilfry

Full-Stack Developer with 3+ years of experience building scalable web applications

0%