DEV Community

Ali
Ali

Posted on

Building a REST API with bunWay in 10 Minutes

TL;DR: Let's build a complete REST API with bunWay—CRUD operations, middleware, error handling, and database integration. If you know Express, you'll feel right at home.


bunWay is an Express-compatible web framework for Bun. Same familiar API, but running on Bun's fast runtime.

Let's build a real API from scratch.

Prerequisites

  • Bun installed (curl -fsSL https://bun.sh/install | bash)
  • Basic JavaScript/TypeScript knowledge
  • 10 minutes

Step 1: Project Setup

# Create project directory
mkdir bunway-api && cd bunway-api

# Initialize project
bun init -y

# Install bunWay
bun add bunway
Enter fullscreen mode Exit fullscreen mode

Step 2: Basic Server

Create index.ts:

import { bunway, json, cors, logger } from 'bunway'

const app = bunway()

// Middleware
app.use(logger('dev'))
app.use(cors())
app.use(json())

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: Date.now() })
})

// Start server
app.listen(3000, () => {
  console.log('API running at http://localhost:3000')
})
Enter fullscreen mode Exit fullscreen mode

Run it:

bun run index.ts
Enter fullscreen mode Exit fullscreen mode

Test it:

curl http://localhost:3000/health
# {"status":"ok","timestamp":1706547200000}
Enter fullscreen mode Exit fullscreen mode

You have a running API.

Step 3: In-Memory Data Store

For this tutorial, we'll use an in-memory store. Replace with your database of choice in production.

Add to index.ts:

// Types
interface User {
  id: string
  name: string
  email: string
  createdAt: number
}

// In-memory store
const users: Map<string, User> = new Map()

// Helper to generate IDs
const generateId = () => Math.random().toString(36).substring(2, 9)
Enter fullscreen mode Exit fullscreen mode

Step 4: CRUD Routes

Now let's add the user routes:

// GET all users
app.get('/api/users', (req, res) => {
  const allUsers = Array.from(users.values())
  res.json(allUsers)
})

// GET single user
app.get('/api/users/:id', (req, res) => {
  const user = users.get(req.params.id)

  if (!user) {
    return res.status(404).json({ error: 'User not found' })
  }

  res.json(user)
})

// POST create user
app.post('/api/users', (req, res) => {
  const { name, email } = req.body

  // Validation
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' })
  }

  // Check for duplicate email
  const existingUser = Array.from(users.values()).find(u => u.email === email)
  if (existingUser) {
    return res.status(409).json({ error: 'Email already exists' })
  }

  // Create user
  const user: User = {
    id: generateId(),
    name,
    email,
    createdAt: Date.now()
  }

  users.set(user.id, user)
  res.status(201).json(user)
})

// PUT update user
app.put('/api/users/:id', (req, res) => {
  const user = users.get(req.params.id)

  if (!user) {
    return res.status(404).json({ error: 'User not found' })
  }

  const { name, email } = req.body

  // Update fields if provided
  if (name) user.name = name
  if (email) user.email = email

  users.set(user.id, user)
  res.json(user)
})

// DELETE user
app.delete('/api/users/:id', (req, res) => {
  const user = users.get(req.params.id)

  if (!user) {
    return res.status(404).json({ error: 'User not found' })
  }

  users.delete(req.params.id)
  res.status(204).send('')
})
Enter fullscreen mode Exit fullscreen mode

Step 5: Error Handling

Add a global error handler:

import { bunway, json, cors, logger, errorHandler } from 'bunway'

// ... your routes ...

// 404 handler (add after all routes)
app.use((req, res) => {
  res.status(404).json({ error: 'Not found' })
})

// Error handler (add last)
app.use(errorHandler())
Enter fullscreen mode Exit fullscreen mode

Complete Code

Here's the full index.ts:

import { bunway, json, cors, logger, errorHandler } from 'bunway'

const app = bunway()

// Types
interface User {
  id: string
  name: string
  email: string
  createdAt: number
}

// In-memory store
const users: Map<string, User> = new Map()
const generateId = () => Math.random().toString(36).substring(2, 9)

// Middleware
app.use(logger('dev'))
app.use(cors())
app.use(json())

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: Date.now() })
})

// GET all users
app.get('/api/users', (req, res) => {
  const allUsers = Array.from(users.values())
  res.json(allUsers)
})

// GET single user
app.get('/api/users/:id', (req, res) => {
  const user = users.get(req.params.id)
  if (!user) {
    return res.status(404).json({ error: 'User not found' })
  }
  res.json(user)
})

// POST create user
app.post('/api/users', (req, res) => {
  const { name, email } = req.body

  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' })
  }

  const existingUser = Array.from(users.values()).find(u => u.email === email)
  if (existingUser) {
    return res.status(409).json({ error: 'Email already exists' })
  }

  const user: User = {
    id: generateId(),
    name,
    email,
    createdAt: Date.now()
  }

  users.set(user.id, user)
  res.status(201).json(user)
})

// PUT update user
app.put('/api/users/:id', (req, res) => {
  const user = users.get(req.params.id)
  if (!user) {
    return res.status(404).json({ error: 'User not found' })
  }

  const { name, email } = req.body
  if (name) user.name = name
  if (email) user.email = email

  users.set(user.id, user)
  res.json(user)
})

// DELETE user
app.delete('/api/users/:id', (req, res) => {
  const user = users.get(req.params.id)
  if (!user) {
    return res.status(404).json({ error: 'User not found' })
  }

  users.delete(req.params.id)
  res.status(204).send('')
})

// 404 handler
app.use((req, res) => {
  res.status(404).json({ error: 'Not found' })
})

// Error handler
app.use(errorHandler())

// Start server
app.listen(3000, () => {
  console.log('API running at http://localhost:3000')
})
Enter fullscreen mode Exit fullscreen mode

Testing the API

Start the server:

bun run index.ts
Enter fullscreen mode Exit fullscreen mode

Test with curl:

# Create a user
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'

# Response: {"id":"abc123","name":"Alice","email":"alice@example.com","createdAt":1706547200000}

# Get all users
curl http://localhost:3000/api/users

# Get single user
curl http://localhost:3000/api/users/abc123

# Update user
curl -X PUT http://localhost:3000/api/users/abc123 \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Smith"}'

# Delete user
curl -X DELETE http://localhost:3000/api/users/abc123
Enter fullscreen mode Exit fullscreen mode

Bonus: Adding Authentication Middleware

Here's a simple JWT-style auth middleware:

// Auth middleware
const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1]

  if (!token) {
    return res.status(401).json({ error: 'No token provided' })
  }

  // In production, verify the token properly
  if (token !== 'secret-token') {
    return res.status(403).json({ error: 'Invalid token' })
  }

  // Attach user info to request
  req.user = { id: '1', role: 'admin' }
  next()
}

// Protected route
app.get('/api/protected', authMiddleware, (req, res) => {
  res.json({ message: 'Secret data', user: req.user })
})
Enter fullscreen mode Exit fullscreen mode

Bonus: Rate Limiting

import { bunway, json, cors, logger, rateLimit } from 'bunway'

// Apply to all routes
app.use(rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100 // 100 requests per minute
}))

// Or apply to specific routes
app.post('/api/users', rateLimit({ windowMs: 60000, max: 10 }), (req, res) => {
  // Max 10 user creations per minute
})
Enter fullscreen mode Exit fullscreen mode

Bonus: Database Integration

Replace the in-memory store with a real database. Here's a Prisma example:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

app.get('/api/users', async (req, res) => {
  const users = await prisma.user.findMany()
  res.json(users)
})

app.post('/api/users', async (req, res) => {
  const { name, email } = req.body
  const user = await prisma.user.create({
    data: { name, email }
  })
  res.status(201).json(user)
})
Enter fullscreen mode Exit fullscreen mode

Project Structure for Larger Apps

bunway-api/
├── src/
│   ├── index.ts          # Entry point
│   ├── routes/
│   │   ├── users.ts      # User routes
│   │   └── posts.ts      # Post routes
│   ├── middleware/
│   │   ├── auth.ts       # Auth middleware
│   │   └── validate.ts   # Validation middleware
│   ├── services/
│   │   └── user.ts       # Business logic
│   └── types/
│       └── index.ts      # TypeScript types
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Example route file (src/routes/users.ts):

import { bunway } from 'bunway'

const router = bunway.Router()

router.get('/', (req, res) => {
  // Get all users
})

router.get('/:id', (req, res) => {
  // Get user by ID
})

export default router
Enter fullscreen mode Exit fullscreen mode

Mount in index.ts:

import userRoutes from './routes/users'

app.use('/api/users', userRoutes)
Enter fullscreen mode Exit fullscreen mode

What's Next?

You now have a working REST API with:

  • CRUD operations
  • Request logging
  • CORS support
  • JSON parsing
  • Error handling

To go further:

  • Add a real database (Prisma, Drizzle, etc.)
  • Implement JWT authentication
  • Add request validation (Zod, etc.)
  • Write tests with bun test
  • Deploy to a server or serverless platform

Resources


Questions? Drop a comment or open an issue on GitHub.

Tags: #bun #javascript #typescript #api #tutorial #backend #webdev #beginners

Top comments (0)