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.jsonBasic 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 codePerformance 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))
}
})
})
}
}



