Lesson 06-Bun.js REST API and Real-Time Chat

REST API Design and Implementation

Basic RESTful API Architecture

Project Structure:

bun-chat-api/
├── src/
   ├── controllers/      # Business logic
   ├── models/           # Data models
   ├── routes/           # Route definitions
   ├── services/         # Service layer
   ├── utils/            # Utility functions
   └── app.ts            # Application entry point
├── bunfig.toml           # Bun configuration
└── package.json

Basic API Implementation:

// src/controllers/userController.ts
import { Context } from 'bun' // Assuming Bun provides an Express-like Context type

export class UserController {
  async getUsers(ctx: Context) {
    // Simulate database query
    const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
    ctx.json({ data: users })
  }

  async createUser(ctx: Context) {
    const userData = await ctx.request.json()
    // Simulate database insert
    const newUser = { id: Date.now(), ...userData }
    ctx.json({ data: newUser }, 201)
  }
}

// src/routes/userRoutes.ts
import { Router } from 'bun' // Assuming Bun provides Router
import { UserController } from '../controllers/userController'

export function userRoutes(router: Router) {
  const controller = new UserController()
  router.get('/users', controller.getUsers.bind(controller))
  router.post('/users', controller.createUser.bind(controller))
}

Database Integration

MySQL Integration Example:

// src/services/userService.ts
import mysql from 'mysql2/promise'

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'chat_db'
})

export class UserService {
  async getUsers() {
    const [rows] = await pool.execute('SELECT * FROM users')
    return rows
  }

  async createUser(userData: { name: string; email: string }) {
    const [result] = await pool.execute(
      'INSERT INTO users (name, email) VALUES (?, ?)',
      [userData.name, userData.email]
    )
    return { id: result.insertId, ...userData }
  }
}

// Updated UserController
export class UserController {
  constructor(private userService: UserService) {}

  async getUsers(ctx: Context) {
    const users = await this.userService.getUsers()
    ctx.json({ data: users })
  }

  async createUser(ctx: Context) {
    const userData = await ctx.request.json()
    const newUser = await this.userService.createUser(userData)
    ctx.json({ data: newUser }, 201)
  }
}

API Documentation and Testing

Swagger Integration:

// src/middleware/swagger.ts
import { Context, Next } from 'bun'

export function swaggerMiddleware(ctx: Context, next: Next) {
  if (ctx.request.url === '/api-docs') {
    ctx.type('json')
    ctx.body = {
      openapi: '3.0.0',
      info: { title: 'Chat API', version: '1.0.0' },
      paths: {
        '/users': {
          get: { summary: 'Get users', responses: { 200: { description: 'OK' } } },
          post: { summary: 'Create user', responses: { 201: { description: 'Created' } } }
        }
      }
    }
    return
  }
  await next()
}

// Usage in app.ts
import { Application } from 'bun'
import { swaggerMiddleware } from './middleware/swagger'

const app = new Application()
app.use(swaggerMiddleware)

Test Cases:

// test/userController.test.ts
import { describe, it, expect, beforeEach } from 'bun:test'
import { UserController } from '../src/controllers/userController'
import { UserService } from '../src/services/userService'

describe('UserController', () => {
  let controller: UserController
  let mockUserService: Partial<UserService>

  beforeEach(() => {
    mockUserService = {
      getUsers: async () => [{ id: 1, name: 'Test User' }],
      createUser: async (data: any) => ({ id: 1, ...data })
    }
    controller = new UserController(mockUserService as UserService)
  })

  it('should get users', async () => {
    const ctx = { json: jest.fn() } as unknown as Context
    await controller.getUsers(ctx)
    expect(ctx.json).toHaveBeenCalledWith({ data: [{ id: 1, name: 'Test User' }] })
  })
})

Real-Time Chat System Implementation

WebSocket Chat Server

Basic Chat Server:

// src/services/chatService.ts
import { WebSocket } from 'bun'

type Client = {
  socket: WebSocket
  userId: string
}

export class ChatService {
  private clients = new Map<string, Client>() // userId -> Client

  handleConnection(socket: WebSocket) {
    let currentUserId: string | null = null

    socket.onopen = () => {
      console.log('WebSocket connected')
    }

    socket.onmessage = async (event) => {
      try {
        const message = JSON.parse(event.data)

        switch (message.type) {
          case 'AUTH':
            currentUserId = message.userId
            this.clients.set(currentUserId, { socket, userId: currentUserId })
            this.broadcastUserList()
            break

          case 'MESSAGE':
            if (!currentUserId) return
            this.broadcastMessage({
              type: 'MESSAGE',
              sender: currentUserId,
              content: message.content,
              timestamp: Date.now()
            })
            break
        }
      } catch (err) {
        console.error('Message error:', err)
      }
    }

    socket.onclose = () => {
      if (currentUserId) {
        this.clients.delete(currentUserId)
        this.broadcastUserList()
      }
    }
  }

  private broadcastUserList() {
    const userList = Array.from(this.clients.keys())
    const message = JSON.stringify({ type: 'USER_LIST', users: userList })

    this.clients.forEach(({ socket }) => {
      if (socket.readyState === WebSocket.OPEN) {
        socket.send(message)
      }
    })
  }

  private broadcastMessage(message: any) {
    const messageStr = JSON.stringify(message)

    this.clients.forEach(({ socket }) => {
      if (socket.readyState === WebSocket.OPEN) {
        socket.send(messageStr)
      }
    })
  }
}

Integrating REST and WebSocket

Unified Application Entry Point:

// src/app.ts
import { Application, Router } from 'bun'
import { userRoutes } from './routes/userRoutes'
import { ChatService } from './services/chatService'

const app = new Application()
const chatService = new ChatService()

// REST API routes
const apiRouter = new Router()
userRoutes(apiRouter)
app.use('/api', apiRouter)

// WebSocket handling
app.upgrade('/ws', (request, socket) => {
  if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
    return new Response('Expected WebSocket upgrade', { status: 400 })
  }

  const { socket: wsSocket } = Bun.upgradeWebSocket(request)
  chatService.handleConnection(wsSocket)

  return new Response(null, { status: 101 }) // Switching Protocols
})

// Start server
app.listen(3000, () => {
  console.log('Server running on http://localhost:3000')
  console.log('WebSocket available at ws://localhost:3000/ws')
})

Message Persistence

Chat Message Storage:

// src/services/messageService.ts
import mysql from 'mysql2/promise'

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'chat_db'
})

export class MessageService {
  async saveMessage(senderId: string, content: string) {
    await pool.execute(
      'INSERT INTO messages (sender_id, content, created_at) VALUES (?, ?, NOW())',
      [senderId, content]
    )
  }

  async getRecentMessages(limit = 50) {
    const [rows] = await pool.execute(
      'SELECT m.*, u.name as sender_name FROM messages m JOIN users u ON m.sender_id = u.id ORDER BY m.created_at DESC LIMIT ?',
      [limit]
    )
    return rows.reverse() // Sort by ascending time
  }
}

// Updated ChatService to support message persistence
export class ChatService {
  constructor(private messageService: MessageService) {}

  private broadcastMessage(message: any) {
    // Save to database
    if (message.type === 'MESSAGE') {
      this.messageService.saveMessage(message.sender, message.content)
    }
    
    // Broadcast message...
  }
}

System Integration and Optimization

Complete Application Architecture

Dependency Injection Container:

// src/container.ts
import { ChatService } from './services/chatService'
import { MessageService } from './services/messageService'
import { UserService } from './services/userService'

export function createContainer() {
  const messageService = new MessageService()
  const userService = new UserService()
  const chatService = new ChatService(messageService) // Inject MessageService

  return {
    chatService,
    messageService,
    userService
  }
}

// Usage in app.ts
import { createContainer } from './container'

const container = createContainer()
const app = new Application()
const chatService = container.chatService

// ...remaining initialization code

Performance Optimization Strategies

WebSocket Connection Optimization:

// src/services/chatService.ts
export class ChatService {
  private clients = new Map<string, Client>()
  private messageQueue = new Map<string, any[]>() // Message queue

  handleConnection(socket: WebSocket) {
    // ...existing code...

    // Message batching
    const batchInterval = setInterval(() => {
      this.clients.forEach(({ socket }, userId) => {
        if (this.messageQueue.has(userId)) {
          const messages = this.messageQueue.get(userId)
          if (messages.length > 0) {
            socket.send(JSON.stringify({
              type: 'BATCH_MESSAGE',
              messages: messages.splice(0, messages.length)
            }))
          }
        }
      })
    }, 100) // Batch send every 100ms

    socket.onclose = () => {
      clearInterval(batchInterval)
      // ...remaining cleanup code...
    }
  }

  private broadcastMessage(message: any) {
    // ...existing code...

    // Use queue storage instead
    this.clients.forEach(({ socket, userId }) => {
      if (socket.readyState === WebSocket.OPEN) {
        if (!this.messageQueue.has(userId)) {
          this.messageQueue.set(userId, [])
        }
        this.messageQueue.get(userId)?.push(message)
      }
    })
  }
}

Security and Authentication

JWT Authentication Integration:

// src/middleware/auth.ts
import { Context, Next } from 'bun'
import jwt from 'jsonwebtoken'

const SECRET = 'your-secret-key'

export function authMiddleware(ctx: Context, next: Next) {
  // REST API authentication
  if (ctx.request.headers.get('authorization')) {
    const token = ctx.request.headers.get('authorization')?.split(' ')[1]
    try {
      const decoded = jwt.verify(token, SECRET)
      ctx.state.user = decoded
    } catch (err) {
      ctx.status = 401
      ctx.json({ error: 'Unauthorized' })
      return
    }
  }

  // WebSocket authentication (handled during connection)
  if (ctx.upgrade?.request.headers.get('authorization')) {
    const token = ctx.upgrade.request.headers.get('authorization')?.split(' ')[1]
    try {
      const decoded = jwt.verify(token, SECRET)
      (ctx as any).state.user = decoded // Pass to WebSocket handler
    } catch (err) {
      ctx.upgrade.response.status = 401
      ctx.upgrade.response.body = 'Unauthorized'
      return
    }
  }

  next()
}

// Updated ChatService to use authentication
export class ChatService {
  handleConnection(socket: WebSocket, userId?: string) {
    // Use passed userId (parsed from JWT)
    if (userId) {
      this.clients.set(userId, { socket, userId })
      this.broadcastUserList()
    }
    // ...remaining code...
  }
}

Deployment and Scaling

Containerized Deployment

Dockerfile Example:

FROM node:18-alpine as builder

WORKDIR /app
COPY . .
RUN bun install --production
RUN bun build src/app.ts --outfile dist/app.js

FROM node:18-alpine

WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
RUN bun install --production

EXPOSE 3000
CMD ["bun", "run", "--bun", "dist/app.js"]

Horizontal Scaling Strategies

Redis Adapter Example:

// src/services/redisAdapter.ts
import { Redis } from 'ioredis'

const redis = new Redis()

export class RedisAdapter {
  async publish(channel: string, message: string) {
    await redis.publish(channel, message)
  }

  async subscribe(channel: string, callback: (message: string) => void) {
    redis.subscribe(channel)
    redis.on('message', (ch, msg) => {
      if (ch === channel) {
        callback(msg)
      }
    })
  }
}

// Updated ChatService to support multiple instances
export class ChatService {
  constructor(private redis: RedisAdapter) {}

  private broadcastMessage(message: any) {
    // Send to local clients directly
    this.clients.forEach(({ socket }) => {
      if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify(message))
      }
    })

    // Broadcast to other instances via Redis
    this.redis.publish('chat_messages', JSON.stringify(message))
  }

  async init() {
    // Subscribe to Redis messages
    this.redis.subscribe('chat_messages', (message) => {
      const parsed = JSON.parse(message)
      // Avoid sending to local clients again (requires more complex deduplication logic)
      this.clients.forEach(({ socket }) => {
        if (socket.readyState === WebSocket.OPEN) {
          socket.send(JSON.stringify(parsed))
        }
      })
    })
  }
}
Share your love