├── config ├── index.ts ├── db.config.ts └── compress.config.ts ├── controllers ├── index.ts └── user.controllers.ts ├── routes ├── index.ts └── user.routes.ts ├── utils ├── index.ts └── genToken.ts ├── .gitignore ├── middlewares ├── index.ts ├── error.middlewares.ts └── auth.middlewares.ts ├── models ├── index.ts └── user.model.ts ├── .env.example ├── tsconfig.json ├── .vscode └── settings.json ├── package.json ├── LICENSE ├── server.ts ├── bun.lock ├── README.md └── components └── ApiDoc.tsx /config/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DB } from './db.config' 2 | -------------------------------------------------------------------------------- /controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.controllers' 2 | -------------------------------------------------------------------------------- /routes/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Users } from './user.routes' 2 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as genToken } from './genToken' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | node_modules/ 3 | 4 | # environment file 5 | .env* 6 | !*.env.example -------------------------------------------------------------------------------- /middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.middlewares' 2 | export * from './error.middlewares' 3 | -------------------------------------------------------------------------------- /models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.model' 2 | export { default as User } from './user.model' 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT="9000" 2 | 3 | MONGO_URI="mongodb://localhost:27017/bun-hono-api" 4 | JWT_SECRET="your-secret-key" -------------------------------------------------------------------------------- /utils/genToken.ts: -------------------------------------------------------------------------------- 1 | import { Jwt } from 'hono/utils/jwt' 2 | 3 | const genToken = (id: string) => { 4 | return Jwt.sign({ id }, Bun.env.JWT_SECRET || '') 5 | } 6 | 7 | export default genToken 8 | -------------------------------------------------------------------------------- /config/db.config.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose' 2 | 3 | const DB = async () => { 4 | try { 5 | const mongoUri = Bun.env.MONGO_URI 6 | if (!mongoUri) { 7 | throw new Error('Missing MONGO_URI in environment variables') 8 | } 9 | 10 | const conn = await mongoose.connect(mongoUri, { 11 | autoIndex: true, 12 | }) 13 | 14 | console.log(`✅ MongoDB Connected: ${conn.connection.host}`) 15 | } catch (err: any) { 16 | console.error(`❌ MongoDB Connection Error: ${err.message}`) 17 | process.exit(1) // Exit on failure 18 | } 19 | } 20 | 21 | export default DB 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "types": ["bun-types"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react-jsx", 12 | "jsxImportSource": "hono/jsx", 13 | "baseUrl": "./", 14 | "paths": { 15 | "~/*": ["*"] 16 | }, 17 | "resolveJsonModule": true, 18 | "noUnusedParameters": false, 19 | "noImplicitAny": false, 20 | "allowSyntheticDefaultImports": true 21 | }, 22 | "include": ["**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules", "dist"] 24 | } 25 | -------------------------------------------------------------------------------- /routes/user.routes.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | // 3 | import { 4 | getUsers, 5 | createUser, 6 | loginUser, 7 | getUserById, 8 | getProfile, 9 | editProfile, 10 | } from '~/controllers' 11 | import { isAdmin, protect } from '~/middlewares' 12 | 13 | const users = new Hono() 14 | 15 | // Get All Users 16 | users.get('/', protect, isAdmin, getUsers) 17 | 18 | // Create User 19 | users.post('/', createUser) 20 | 21 | // Login User 22 | users.post('/login', loginUser) 23 | 24 | // Get User Profile 25 | users.get('/profile', protect, getProfile) 26 | 27 | // Edit User Profile 28 | users.put('/profile', protect, editProfile) 29 | 30 | // Get Single User 31 | users.get('/:id', protect, isAdmin, getUserById) 32 | 33 | export default users 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.formatOnPaste": false, 5 | "editor.codeActionsOnSave": ["source.formatDocument", "source.fixAll.eslint"], 6 | "files.exclude": { 7 | // "**/.git": true, 8 | // "**/.svn": true, 9 | // "**/.hg": true, 10 | // "**/CVS": true, 11 | // "**/.DS_Store": true, 12 | // "**/Thumbs.db": true, 13 | // "**/node_modules": true, 14 | // "**/*.d.ts": true, 15 | // "**/*.md": true, 16 | // "*.log": true, 17 | // "**/*.lock": true, 18 | // ".eslintrc": true, 19 | // ".eslintignore": true, 20 | // ".prettierrc": true, 21 | // ".prettierignore": true, 22 | // "**/*node.json": true 23 | // "**/.gitignore": true, 24 | // ".vscode": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bun-hono-api-starter", 3 | "version": "2.0.0", 4 | "description": "A starter template for building APIs with Bun and Hono", 5 | "author": "Mehedi ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ProMehedi/bun-hono-api-starter" 10 | }, 11 | "scripts": { 12 | "start": "NODE_ENV=production bun run server.ts", 13 | "dev": "bun run --hot server.ts", 14 | "typecheck": "tsc --noEmit" 15 | }, 16 | "dependencies": { 17 | "hono": "^4.7.4", 18 | "mongoose": "^8.12.1" 19 | }, 20 | "devDependencies": { 21 | "@types/bun": "latest", 22 | "@types/mongoose": "^5.11.97" 23 | }, 24 | "engines": { 25 | "bun": ">=1.0.0" 26 | }, 27 | "keywords": [ 28 | "bun", 29 | "hono", 30 | "mongodb", 31 | "mongoose", 32 | "typescript", 33 | "api", 34 | "rest", 35 | "starter", 36 | "template" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /middlewares/error.middlewares.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, NotFoundHandler } from 'hono' 2 | import { ContentfulStatusCode, StatusCode } from 'hono/utils/http-status' 3 | 4 | // Error Handler 5 | export const errorHandler: ErrorHandler = (err, c) => { 6 | const currentStatus = 7 | 'status' in err ? err.status : c.newResponse(null).status 8 | 9 | const statusCode = currentStatus !== 200 ? (currentStatus as StatusCode) : 500 10 | const env = c.env?.NODE_ENV || process.env?.NODE_ENV 11 | 12 | return c.json( 13 | { 14 | success: false, 15 | message: err?.message || 'Internal Server Error', 16 | stack: env ? null : err?.stack, 17 | }, 18 | statusCode as ContentfulStatusCode 19 | ) 20 | } 21 | 22 | // Not Found Handler 23 | export const notFound: NotFoundHandler = (c) => { 24 | return c.json( 25 | { 26 | success: false, 27 | message: `Not Found - [${c.req.method}]:[${c.req.url}]`, 28 | }, 29 | 404 // Explicitly set 404 status 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mehedi Hasan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Document, Schema, model } from 'mongoose' 2 | 3 | export interface IUser extends Document { 4 | _id: Schema.Types.ObjectId 5 | name: string 6 | email: string 7 | password: string 8 | isAdmin: boolean 9 | matchPassword: (pass: string) => Promise 10 | } 11 | 12 | const userSchema = new Schema( 13 | { 14 | name: { type: String, required: true }, 15 | email: { type: String, required: true, unique: true, match: /.+\@.+\..+/ }, 16 | password: { type: String, required: true, minlength: 6 }, 17 | isAdmin: { type: Boolean, required: true, default: false }, 18 | }, 19 | { timestamps: true } 20 | ) 21 | 22 | // Match user entered password to hashed password in database 23 | userSchema.methods.matchPassword = async function (enteredPassword: string) { 24 | return await Bun.password.verify(enteredPassword, this.password) 25 | } 26 | 27 | userSchema.pre('save', async function (next) { 28 | if (!this.isModified('password')) { 29 | return next() 30 | } 31 | try { 32 | this.password = await Bun.password.hash(this.password, { 33 | algorithm: 'bcrypt', 34 | cost: 10, // number between 4-31 [Heiger is secure but slower] 35 | }) 36 | next() 37 | } catch (error) { 38 | next(error as Error) 39 | } 40 | }) 41 | 42 | const User = model('User', userSchema) 43 | export default User 44 | -------------------------------------------------------------------------------- /config/compress.config.ts: -------------------------------------------------------------------------------- 1 | import { createGzip, createDeflate, createBrotliCompress } from 'node:zlib' 2 | 3 | function createCompressionStream(format: string) { 4 | let handler 5 | if (format === 'gzip') { 6 | handler = createGzip() 7 | } else if (format === 'deflate') { 8 | handler = createDeflate() 9 | } else if (format === 'br') { 10 | handler = createBrotliCompress() 11 | } else { 12 | throw new Error(`Unsupported compression format: ${format}`) 13 | } 14 | 15 | const readableStream = new ReadableStream({ 16 | start(controller) { 17 | handler.on('data', (chunk) => controller.enqueue(chunk)) 18 | handler.on('end', () => controller.close()) 19 | handler.on('error', (err) => controller.error(err)) 20 | }, 21 | }) 22 | 23 | const writableStream = new WritableStream({ 24 | write(chunk) { 25 | handler.write(chunk) 26 | }, 27 | close() { 28 | handler.end() 29 | }, 30 | }) 31 | 32 | return { readable: readableStream, writable: writableStream } 33 | } 34 | 35 | // ✅ Polyfill `CompressionStream` if missing 36 | if (typeof globalThis.CompressionStream === 'undefined') { 37 | class CompressionStream { 38 | readable: ReadableStream 39 | writable: WritableStream 40 | constructor(format: string) { 41 | const { readable, writable } = createCompressionStream(format) 42 | this.readable = readable 43 | this.writable = writable 44 | } 45 | } 46 | globalThis.CompressionStream = CompressionStream 47 | } 48 | -------------------------------------------------------------------------------- /middlewares/auth.middlewares.ts: -------------------------------------------------------------------------------- 1 | import { Context, Next } from 'hono' 2 | import { HTTPException } from 'hono/http-exception' // Added for better error handling 3 | import { Jwt } from 'hono/utils/jwt' 4 | import { User, IUser } from '~/models' 5 | 6 | // Protect Route for Authenticated Users 7 | export const protect = async (c: Context, next: Next) => { 8 | const authHeader = c.req.header('Authorization') 9 | 10 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 11 | throw new HTTPException(401, { 12 | message: 'Not authorized! No token provided!', 13 | }) 14 | } 15 | 16 | const token = authHeader.replace(/^Bearer\s+/i, '') 17 | 18 | try { 19 | const { id } = await Jwt.verify(token, process.env.JWT_SECRET || '') // Updated from Bun.env 20 | if (!id) { 21 | throw new HTTPException(401, { message: 'Invalid token payload!' }) 22 | } 23 | 24 | const user = await User.findById(id).select('-password') 25 | if (!user) { 26 | throw new HTTPException(401, { message: 'User not found!' }) 27 | } 28 | 29 | // Type-safe user assignment 30 | c.set('user', user as IUser) 31 | await next() 32 | } catch (err) { 33 | throw new HTTPException(401, { message: 'Invalid token! Not authorized!' }) 34 | } 35 | } 36 | 37 | // Check if user is admin 38 | export const isAdmin = async (c: Context, next: Next) => { 39 | const user = c.get('user') as IUser | undefined 40 | 41 | if (!user) { 42 | throw new HTTPException(401, { 43 | message: 'Not authorized! No user context!', 44 | }) 45 | } 46 | 47 | if (user.isAdmin) { 48 | await next() 49 | } else { 50 | throw new HTTPException(403, { message: 'Not authorized as an admin!' }) // 403 for permission denied 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import '~/config/compress.config' 2 | import { Hono } from 'hono' 3 | import { cors } from 'hono/cors' 4 | import { logger } from 'hono/logger' 5 | import { compress } from 'hono/compress' 6 | // 7 | import { DB } from './config' 8 | import { Users } from '~/routes' 9 | import { errorHandler, notFound } from '~/middlewares' 10 | import { ApiDoc } from '~/components/ApiDoc' 11 | 12 | // Initialize the Hono app with base path 13 | const app = new Hono({ strict: false }).basePath('/api/v1') 14 | 15 | // Config MongoDB - Only connect if not in Cloudflare Workers environment 16 | if (typeof process !== 'undefined') { 17 | DB() 18 | } 19 | 20 | // Logger middleware 21 | app.use(logger()) 22 | 23 | // Compress middleware 24 | app.use( 25 | compress({ 26 | encoding: 'gzip', 27 | threshold: 1024, // Minimum size to compress (1KB) 28 | }) 29 | ) 30 | 31 | // CORS configuration (tightened for security) 32 | app.use( 33 | '*', 34 | cors({ 35 | origin: '*', // Specify allowed origins (update for production) 36 | allowMethods: ['GET', 'POST', 'PUT', 'DELETE'], // Specify allowed methods 37 | credentials: true, 38 | maxAge: 86400, // Cache preflight for 1 day 39 | }) 40 | ) 41 | 42 | // Home Route with API Documentation [FOR DEMO PURPOSES] 43 | app.get('/', (c) => { 44 | const apiRoutes = [ 45 | { 46 | method: 'GET', 47 | path: '/api/v1', 48 | description: 'API Documentation', 49 | auth: false, 50 | admin: false, 51 | }, 52 | { 53 | method: 'POST', 54 | path: '/api/v1/users', 55 | description: 'Create a new user', 56 | auth: false, 57 | admin: false, 58 | }, 59 | { 60 | method: 'POST', 61 | path: '/api/v1/users/login', 62 | description: 'User login', 63 | auth: false, 64 | admin: false, 65 | }, 66 | { 67 | method: 'GET', 68 | path: '/api/v1/users/profile', 69 | description: 'Get user profile', 70 | auth: true, 71 | admin: false, 72 | }, 73 | { 74 | method: 'PUT', 75 | path: '/api/v1/users/profile', 76 | description: 'Update user profile', 77 | auth: true, 78 | admin: false, 79 | }, 80 | { 81 | method: 'GET', 82 | path: '/api/v1/users', 83 | description: 'Get all users', 84 | auth: true, 85 | admin: true, 86 | }, 87 | { 88 | method: 'GET', 89 | path: '/api/v1/users/:id', 90 | description: 'Get user by ID', 91 | auth: true, 92 | admin: true, 93 | }, 94 | ] 95 | 96 | return c.html( 97 | ApiDoc({ 98 | title: 'Bun + Hono API Starter', 99 | version: '2.0.0', 100 | routes: apiRoutes, 101 | }) 102 | ) 103 | }) 104 | 105 | // User Routes 106 | app.route('/users', Users) 107 | 108 | // Error Handler (improved to use err) 109 | app.onError(errorHandler) 110 | 111 | // Not Found Handler (standardized response) 112 | app.notFound(notFound) 113 | 114 | // Determine the environment 115 | const port = process.env?.PORT || 8000 116 | 117 | // Export for both Bun and Cloudflare Workers 118 | export default { 119 | port, 120 | fetch: app.fetch, 121 | } 122 | -------------------------------------------------------------------------------- /controllers/user.controllers.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'hono' 2 | import { HTTPException } from 'hono/http-exception' 3 | // 4 | import { genToken } from '~/utils' 5 | import { IUser, User } from '~/models' 6 | 7 | /** 8 | * @api {get} /users Get All Users 9 | * @apiGroup Users 10 | * @access Private 11 | */ 12 | export const getUsers = async (c: Context) => { 13 | const users = await User.find() 14 | 15 | return c.json({ users }) 16 | } 17 | 18 | /** 19 | * @api {post} /users Create User 20 | * @apiGroup Users 21 | * @access Public 22 | */ 23 | export const createUser = async (c: Context) => { 24 | const { name, email, password, isAdmin } = await c.req.json() 25 | 26 | // Check for existing user 27 | const userExists = await User.findOne({ email }) 28 | if (userExists) { 29 | throw new HTTPException(400, { message: 'User already exists' }) 30 | } 31 | 32 | const user: IUser = await User.create({ 33 | name, 34 | email, 35 | password, 36 | isAdmin, 37 | }) 38 | 39 | if (!user) { 40 | throw new HTTPException(400, { message: 'Invalid user data' }) 41 | } 42 | 43 | const token = await genToken(user._id.toString()) 44 | 45 | return c.json({ 46 | success: true, 47 | data: { 48 | _id: user._id, 49 | name: user.name, 50 | email: user.email, 51 | isAdmin: user.isAdmin, 52 | token, 53 | }, 54 | message: 'User created successfully', 55 | }) 56 | } 57 | 58 | /** 59 | * @api {post} /users/login Login User 60 | * @apiGroup Users 61 | * @access Public 62 | */ 63 | export const loginUser = async (c: Context) => { 64 | const { email, password } = await c.req.json() 65 | 66 | // Check for missing email or password 67 | if (!email || !password) { 68 | throw new HTTPException(400, { 69 | message: 'Please provide an email and password', 70 | }) 71 | } 72 | 73 | const user = await User.findOne({ email }) 74 | if (!user) { 75 | throw new HTTPException(401, { message: 'No user found with this email' }) 76 | } 77 | 78 | // Fixed typo: 'mathPassword' -> 'matchPassword' 79 | if (!(await user.matchPassword(password))) { 80 | throw new HTTPException(401, { message: 'Invalid credentials' }) 81 | } 82 | 83 | const token = await genToken(user._id.toString()) 84 | 85 | return c.json({ 86 | success: true, 87 | data: { 88 | _id: user._id, 89 | name: user.name, 90 | email: user.email, 91 | isAdmin: user.isAdmin, 92 | token, 93 | }, 94 | message: 'User logged in successfully', 95 | }) 96 | } 97 | 98 | /** 99 | * @api {get} /users/:id Get Single User 100 | * @apiGroup Users 101 | * @access Private 102 | */ 103 | export const getUserById = async (c: Context) => { 104 | const user = await User.findById(c.req.param('id')).select('-password') 105 | 106 | if (!user) { 107 | throw new HTTPException(404, { message: 'User not found' }) 108 | } 109 | 110 | return c.json({ user }) 111 | } 112 | 113 | /** 114 | * @api {get} /users/profile Get User Profile 115 | * @apiGroup Users 116 | * @access Private 117 | */ 118 | export const getProfile = async (c: Context) => { 119 | const user = c.get('user') as IUser 120 | 121 | return c.json({ user }) 122 | } 123 | 124 | /** 125 | * @api {put} /users/profile Edit User Profile 126 | * @apiGroup Users 127 | * @access Private 128 | */ 129 | export const editProfile = async (c: Context) => { 130 | const user = c.get('user') as IUser 131 | const { name, email, password } = await c.req.json() 132 | 133 | if (name) user.name = name 134 | if (email) user.email = email 135 | if (password) user.password = password 136 | 137 | await user.save() 138 | 139 | return c.json({ user }) 140 | } 141 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "bun-hono-api-starter", 6 | "dependencies": { 7 | "hono": "^4.7.4", 8 | "mongoose": "^8.12.1", 9 | }, 10 | "devDependencies": { 11 | "@types/bun": "latest", 12 | "@types/mongoose": "^5.11.97", 13 | }, 14 | }, 15 | }, 16 | "packages": { 17 | "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.2.0", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg=="], 18 | 19 | "@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="], 20 | 21 | "@types/mongoose": ["@types/mongoose@5.11.97", "", { "dependencies": { "mongoose": "*" } }, "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q=="], 22 | 23 | "@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], 24 | 25 | "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], 26 | 27 | "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], 28 | 29 | "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], 30 | 31 | "bson": ["bson@6.10.3", "", {}, "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ=="], 32 | 33 | "bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="], 34 | 35 | "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 36 | 37 | "hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], 38 | 39 | "kareem": ["kareem@2.6.3", "", {}, "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q=="], 40 | 41 | "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], 42 | 43 | "mongodb": ["mongodb@6.14.2", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.3", "mongodb-connection-string-url": "^3.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q=="], 44 | 45 | "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], 46 | 47 | "mongoose": ["mongoose@8.12.1", "", { "dependencies": { "bson": "^6.10.3", "kareem": "2.6.3", "mongodb": "~6.14.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q=="], 48 | 49 | "mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="], 50 | 51 | "mquery": ["mquery@5.0.0", "", { "dependencies": { "debug": "4.x" } }, "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg=="], 52 | 53 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 54 | 55 | "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 56 | 57 | "sift": ["sift@17.1.3", "", {}, "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="], 58 | 59 | "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], 60 | 61 | "tr46": ["tr46@5.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g=="], 62 | 63 | "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], 64 | 65 | "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], 66 | 67 | "whatwg-url": ["whatwg-url@14.1.1", "", { "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ=="], 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bun + Hono API Starter 2 | 3 | A modern, high-performance API starter template using [Bun](https://bun.sh), [Hono](https://hono.dev), [MongoDB](https://mongodb.com), and TypeScript. 4 | 5 | ## Features 6 | 7 | - ⚡️ **Ultra-fast performance** with Bun runtime 8 | - 🔄 **Hot reloading** for fast development cycles 9 | - 🧩 **Modular architecture** for scalability 10 | - 🔒 **Built-in authentication** middleware and JWT support 11 | - 🚦 **Request validation** for robust API design 12 | - 🗃️ **MongoDB integration** with Mongoose 13 | - 📦 **Compression support** for optimized responses 14 | - ✅ **TypeScript** for type safety 15 | - 🔍 **Error handling** middleware 16 | 17 | ## Table of Contents 18 | 19 | - [Getting Started](#getting-started) 20 | - [Prerequisites](#prerequisites) 21 | - [Installation](#installation) 22 | - [Configuration](#configuration) 23 | - [Usage](#usage) 24 | - [Development](#development) 25 | - [Production](#production) 26 | - [API Routes](#api-routes) 27 | - [User Model](#user-model) 28 | - [Project Structure](#project-structure) 29 | - [Changelog](#changelog) 30 | - [Contributing](#contributing) 31 | - [License](#license) 32 | - [Contact](#contact) 33 | 34 | ## Getting Started 35 | 36 | ### Prerequisites 37 | 38 | Before you begin, make sure you have the following installed: 39 | 40 | - [Bun](https://bun.sh) (v1.0.0 or newer) 41 | - [MongoDB](https://mongodb.com) or [MongoDB Atlas](https://www.mongodb.com/atlas/database) 42 | 43 | ### Installation 44 | 45 | 1. Clone this repository: 46 | 47 | ```bash 48 | git clone https://github.com/ProMehedi/bun-hono-api-starter.git 49 | cd bun-hono-api-starter 50 | ``` 51 | 52 | 2. Install dependencies: 53 | 54 | ```bash 55 | bun install 56 | ``` 57 | 58 | ### Configuration 59 | 60 | Create a `.env` file in the root directory with the following variables: 61 | 62 | ``` 63 | PORT=8000 64 | MONGO_URI=mongodb://localhost:27017/bun-hono-api 65 | JWT_SECRET=your_jwt_secret_key 66 | ``` 67 | 68 | ## Usage 69 | 70 | ### Development 71 | 72 | Run the development server with hot reloading: 73 | 74 | ```bash 75 | bun dev 76 | ``` 77 | 78 | ### Production 79 | 80 | Start the production server: 81 | 82 | ```bash 83 | bun start 84 | ``` 85 | 86 | ## API Routes 87 | 88 | | Method | Route | Description | Auth Required | Admin Only | 89 | | ------ | ----------------------- | ------------------- | ------------- | ---------- | 90 | | GET | `/api/v1` | API welcome message | No | No | 91 | | POST | `/api/v1/users` | Create a new user | No | No | 92 | | POST | `/api/v1/users/login` | User login | No | No | 93 | | GET | `/api/v1/users/profile` | Get user profile | Yes | No | 94 | | PUT | `/api/v1/users/profile` | Update user profile | Yes | No | 95 | | GET | `/api/v1/users` | Get all users | Yes | Yes | 96 | | GET | `/api/v1/users/:id` | Get user by ID | Yes | Yes | 97 | 98 | ### Request/Response Examples 99 | 100 | **Create User:** 101 | 102 | ``` 103 | POST /api/v1/users 104 | ``` 105 | 106 | ```json 107 | { 108 | "name": "Mehedi Hasan", 109 | "email": "mehedi@example.com", 110 | "password": "123456" 111 | } 112 | ``` 113 | 114 | **User Login:** 115 | 116 | ``` 117 | POST /api/v1/users/login 118 | ``` 119 | 120 | ```json 121 | { 122 | "email": "mehedi@example.com", 123 | "password": "123456" 124 | } 125 | ``` 126 | 127 | **Update Profile:** 128 | 129 | ``` 130 | PUT /api/v1/users/profile 131 | ``` 132 | 133 | ```json 134 | { 135 | "name": "Updated Name", 136 | "email": "updated@example.com", 137 | "password": "newpassword" // Optional 138 | } 139 | ``` 140 | 141 | **Protected Routes:** 142 | Include the JWT token in the Authorization header: 143 | 144 | ``` 145 | Authorization: Bearer your_jwt_token 146 | ``` 147 | 148 | ## User Model 149 | 150 | The user model includes the following properties: 151 | 152 | ```typescript 153 | interface IUser extends Document { 154 | _id: Schema.Types.ObjectId 155 | name: string 156 | email: string 157 | password: string 158 | isAdmin: boolean 159 | matchPassword: (pass: string) => Promise 160 | } 161 | ``` 162 | 163 | Key features: 164 | 165 | - Password hashing with Bun's built-in password utilities 166 | - Automatic email validation (must match email pattern) 167 | - Admin role support with the `isAdmin` property 168 | - Password matching method for authentication 169 | 170 | ## Project Structure 171 | 172 | ``` 173 | ├── config/ # Configuration files 174 | │ ├── compress.config.ts # Compression configuration 175 | │ ├── db.config.ts # Database configuration 176 | │ └── index.ts # Config exports 177 | ├── controllers/ # Route controllers 178 | │ ├── user.controllers.ts # User-related controllers 179 | │ └── index.ts # Controller exports 180 | ├── middlewares/ # Express middlewares 181 | │ ├── auth.middlewares.ts # Authentication middleware 182 | │ ├── error.middlewares.ts # Error handling middleware 183 | │ └── index.ts # Middleware exports 184 | ├── models/ # Database models 185 | │ ├── user.model.ts # User model schema 186 | │ └── index.ts # Model exports 187 | ├── routes/ # API routes 188 | │ ├── user.routes.ts # User routes 189 | │ └── index.ts # Route exports 190 | ├── utils/ # Utility functions 191 | │ ├── genToken.ts # JWT token generator 192 | │ └── index.ts # Utils exports 193 | ├── server.ts # Main application entry 194 | ├── .env # Environment variables (create this) 195 | ├── .gitignore # Git ignore file 196 | ├── bun.lock # Bun lock file 197 | ├── package.json # Package configuration 198 | ├── README.md # This file 199 | ├── tsconfig.json # TypeScript configuration 200 | └── wrangler.toml # Cloudflare Workers configuration 201 | ``` 202 | 203 | ## Changelog 204 | 205 | ### Version 2.0.0 206 | 207 | - Complete project restructuring with improved modularity 208 | - Added compression support with polyfill for `CompressionStream` 209 | - Enhanced error handling middleware 210 | - Updated MongoDB connection with better error feedback 211 | - Improved CORS configuration for better security 212 | - Updated to latest Hono v4.7.4 and Mongoose v8.12.1 213 | - Enhanced TypeScript support and typings 214 | - Standardized export patterns across modules 215 | - Added admin role functionality with middleware protection 216 | - Added profile editing functionality 217 | 218 | ### Version 1.0.0 219 | 220 | - Initial release with basic CRUD functionality 221 | - MongoDB integration 222 | - JWT-based authentication 223 | - Basic error handling 224 | 225 | ## Contributing 226 | 227 | Contributions are welcome! Please feel free to submit a Pull Request. 228 | 229 | 1. Fork the repository 230 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 231 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 232 | 4. Push to the branch (`git push origin feature/amazing-feature`) 233 | 5. Open a Pull Request 234 | 235 | ## License 236 | 237 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 238 | 239 | ## Contact 240 | 241 | Mehedi Hasan - [admin@promehedi.com](mailto:admin@promehedi.com) 242 | 243 | Project Link: [https://github.com/ProMehedi/bun-hono-api-starter](https://github.com/ProMehedi/bun-hono-api-starter) 244 | -------------------------------------------------------------------------------- /components/ApiDoc.tsx: -------------------------------------------------------------------------------- 1 | import { html } from 'hono/html' 2 | 3 | interface Route { 4 | method: string 5 | path: string 6 | description: string 7 | auth: boolean 8 | admin: boolean 9 | } 10 | 11 | interface ApiDocProps { 12 | title: string 13 | version: string 14 | routes: Route[] 15 | } 16 | 17 | export const ApiDoc = ({ title, version, routes }: ApiDocProps) => { 18 | const methodColors = { 19 | GET: 'bg-blue-600', 20 | POST: 'bg-green-600', 21 | PUT: 'bg-amber-600', 22 | DELETE: 'bg-red-600', 23 | } 24 | 25 | return html` 26 | 27 | 28 | 29 | 30 | ${title} - API Documentation 31 | 32 | 33 | 34 |
35 |
36 |

${title}

37 |
38 | v${version} 42 | Built with Bun + Hono + MongoDB 45 |
46 |
47 | 48 |
49 |
50 |

53 | API Endpoints 54 |

55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ${routes.map( 68 | (route) => html` 69 | 77 | 84 | 85 | 92 | 99 | ` 100 | )} 101 | 102 |
MethodEndpointDescriptionAuthAdmin
70 | ${route.method} 76 | 78 | ${route.path} 83 | ${route.description} 86 | ${route.auth 87 | ? html`Required` 90 | : html`No`} 91 | 93 | ${route.admin 94 | ? html`Yes` 97 | : html`No`} 98 |
103 |
104 |
105 | 106 |
107 |

110 | Request Examples 111 |

112 | 113 |
114 |

Create User

115 |
116 |
117 | POST 121 | /api/v1/users 124 |
125 |
{
126 |   "name": "Mehedi Hasan",
127 |   "email": "mehedi@example.com",
128 |   "password": "123456"
129 | }
130 |
131 |
132 | 133 |
134 |

Login User

135 |
136 |
137 | POST 141 | /api/v1/users/login 144 |
145 |
{
146 |   "email": "mehedi@example.com",
147 |   "password": "123456"
148 | }
149 |
150 |
151 | 152 |
153 |

154 | Authorization for Protected Routes 155 |

156 |
157 |
158 | Include the JWT token in the Authorization header: 161 |
162 |
Authorization: Bearer your_jwt_token
165 |
166 |
167 |
168 | 169 |
170 |

173 | Project Information 174 |

175 |
    176 |
  • 177 | Author: 178 | Mehedi Hasan 179 |
  • 180 |
  • 181 | GitHub: 182 | bun-hono-api-starter 188 |
  • 189 |
  • 190 | License: 191 | MIT 192 |
  • 193 |
194 |
195 |
196 | 197 | 202 |
203 | 204 | ` 205 | } 206 | --------------------------------------------------------------------------------