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.