├── .gitignore ├── .vscode └── settings.json ├── README.md ├── bun.lockb ├── config ├── db.ts └── index.ts ├── controllers ├── index.ts └── userControllers.ts ├── index.ts ├── middlewares ├── auth.ts ├── error.ts ├── index.ts └── logger.ts ├── models ├── index.ts └── userModel.ts ├── package.json ├── routes ├── index.ts └── userRoutes.ts ├── tsconfig.json ├── types.d.ts └── utils ├── index.ts └── jwt.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env* 29 | !.env.example 30 | 31 | # vercel 32 | .vercel 33 | 34 | **/*.trace 35 | **/*.zip 36 | **/*.tar.gz 37 | **/*.tgz 38 | **/*.log 39 | package-lock.json 40 | **/*.bun -------------------------------------------------------------------------------- /.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 | "*.log": true, 16 | "**/*.lock": true, 17 | "**/*.lockb": true, 18 | "**/*node.json": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REST API starter using Bun + Elysia + MongoDB + TypeScript 2 | 3 | Welcome to my awesome project! This project is a REST API starter using Bun + Elysia + MongoDB + TypeScript providing a powerful and efficient platform with a simple CRUD interface for a user model. 4 | 5 | ## Table of Contents 6 | 7 | - [Getting Started](#getting-started) 8 | - [Installations](#installations) 9 | - [Configuration](#configuration) 10 | - [Routes](#routes) 11 | - [Usage](#usage) 12 | - [Project Structure](#project-structure) 13 | - [Contributing](#contributing) 14 | - [License](#license) 15 | - [Contact](#contact) 16 | 17 | ## Getting Started 18 | 19 | Before you begin, make sure you have the following installed: 20 | 21 | - [Bun](https://bun.sh) 22 | - [MongoDB](mongodb.com) or [MongoCompass](mongodb.com/products/compass) 23 | 24 | ### Installations: 25 | 26 | 1. Clone this repository to your local machine 27 | 28 | ```bash 29 | git clone https://github.com/ProMehedi/elysia-api-starter.git 30 | ``` 31 | 32 | 2. Navigate to the project directory 33 | 34 | ```bash 35 | cd elysia-api-starter 36 | ``` 37 | 38 | 3. Install dependencies 39 | 40 | ```bash 41 | bun install 42 | ``` 43 | 44 | To run: 45 | 46 | ```bash 47 | bun run dev 48 | ``` 49 | 50 | ### Configuration 51 | 52 | Create a .env file in the root directory of your project. Add environment-specific variables on new lines in the form of NAME=VALUE. For example: 53 | 54 | ``` 55 | PORT=9000 56 | MONGO_URI=mongodb://localhost:27017/bun-hono-rest-api 57 | JWT_SECRET=secret 58 | ``` 59 | 60 | ### Routes 61 | 62 | ``` 63 | POST /api/v1/users (Create User) 64 | POST /api/v1/users/login (Login User) 65 | GET /api/v1/users/profile (Get User Profile) 66 | GET /api/v1/useres (Get All Users) 67 | GET /api/v1/users/:id (Get User By Id) 68 | ``` 69 | 70 | ### Usage 71 | 72 | ``` 73 | POST /api/v1/users (Create User) 74 | ``` 75 | 76 | ```json 77 | { 78 | "name": "Mehedi Hasan", 79 | "email": "mehedi@example.com", 80 | "password": "123456" 81 | } 82 | ``` 83 | 84 | ``` 85 | POST /api/v1/users/login (Login User) 86 | ``` 87 | 88 | ```json 89 | { 90 | "email": "mehedi@example.com", 91 | "password": "123456" 92 | } 93 | ``` 94 | 95 | ``` 96 | GET /api/v1/users/profile (Get User Profile) 97 | Authorisation Header (Bearer Token) 98 | ``` 99 | 100 | ``` 101 | GET /api/v1/useres (Get All Users) 102 | Authorisation Header (Bearer Token) 103 | ``` 104 | 105 | ``` 106 | GET /api/v1/users/:id (Get User By Id) 107 | Authorisation Header (Bearer Token) 108 | ``` 109 | 110 | ## Project Structure 111 | 112 | ``` 113 | 114 | ├── .vscode 115 | │ ├── settings.json 116 | ├── config 117 | │ ├── db.ts 118 | ├── controllers 119 | │ ├── index.ts 120 | │ ├── userControllers.ts 121 | ├── middlewares 122 | │ ├── index.ts 123 | │ ├── logger.ts 124 | │ ├── authMiddlewares.ts 125 | │ ├── errorMiddlewares.ts 126 | ├── models 127 | │ ├── index.ts 128 | │ ├── userModels.ts 129 | ├── routes 130 | │ ├── index.ts 131 | │ ├── userRoutes.ts 132 | ├── utils 133 | │ ├── index.ts 134 | │ ├── getToken.ts 135 | ├── index.ts 136 | ├── .env 137 | ├── .gitignore 138 | ├── bun.lockb 139 | ├── README.md 140 | ├── package.json 141 | ├── tsconfig.ts 142 | 143 | ``` 144 | 145 | ## Contributing 146 | 147 | We welcome contributions to improve the API! If you find a bug, have a feature request, or want to suggest improvements, please create an issue in the GitHub repository. If you'd like to contribute code, feel free to fork the repository, create a new branch, commit your changes, and open a pull request. 148 | 149 | Please ensure that your code follows the existing coding style and conventions. 150 | 151 | ## License 152 | 153 | This project is licensed under the [MIT] License 154 | 155 | ## Contact 156 | 157 | If you have any questions or need further assistance, you can reach us at [Mehedi Hasan](fb.com/promehedi). 158 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProMehedi/elysia-api-starter/87e11007267259208cf6fcde4e2d99b89f4735e7/bun.lockb -------------------------------------------------------------------------------- /config/db.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose' 2 | import * as pc from 'picocolors' 3 | 4 | export const connectDB = async () => { 5 | try { 6 | if (Bun.env.MONGO_URI !== undefined) { 7 | const conn = await mongoose.connect(Bun.env.MONGO_URI, { 8 | autoIndex: true, 9 | }) 10 | 11 | console.log( 12 | pc.cyan( 13 | `Success: MongoDB Connected: ${conn.connection.host}:${conn.connection.port} - [${conn.connection.name}]` 14 | ) 15 | ) 16 | } 17 | } catch (err: any) { 18 | console.error(pc.red(`Error: ${err.message}`)) 19 | process.exit(1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './db' 2 | -------------------------------------------------------------------------------- /controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './userControllers' 2 | -------------------------------------------------------------------------------- /controllers/userControllers.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'elysia' 2 | import { User } from '~/models' 3 | import { jwt } from '~/utils' 4 | 5 | /** 6 | * @api [POST] /users 7 | * @description: Create a new user 8 | * @action public 9 | */ 10 | export const createUser = async (c: Context) => { 11 | // Check for body 12 | if (!c.body) throw new Error('No body provided') 13 | 14 | const { name, email, password, isAdmin } = c.body as RegBody 15 | 16 | // Check for user 17 | const userExists = await User.findOne({ email }) 18 | if (userExists) { 19 | c.set.status = 400 20 | throw new Error('User already exists!') 21 | } 22 | 23 | // Create user 24 | const _user = await User.create({ 25 | name, 26 | email, 27 | password, 28 | isAdmin, 29 | }) 30 | 31 | if (!_user) { 32 | c.set.status = 400 33 | throw new Error('Invalid user data!') 34 | } 35 | 36 | // Generate token 37 | const accessToken = await jwt.sign({ 38 | data: { id: _user._id, isAdmin: _user.isAdmin }, 39 | }) 40 | 41 | // Return success response 42 | c.set.status = 201 43 | return { 44 | status: c.set.status, 45 | success: true, 46 | data: { accessToken }, 47 | message: 'User created successfully', 48 | } 49 | } 50 | 51 | /** 52 | * @api [POST] /users/login 53 | * @description: Login a user 54 | * @action public 55 | */ 56 | export const loginUser = async (c: Context) => { 57 | // Check for body 58 | if (!c.body) throw new Error('No body provided') 59 | 60 | const { email, password } = c.body as LoginBody 61 | 62 | if (!email || !password) throw new Error('Invalid email or password!') 63 | 64 | // Check for user 65 | const user = await User.findOne({ email }) 66 | if (!user) { 67 | c.set.status = 401 68 | throw new Error('Invalid email or password!') 69 | } 70 | 71 | // Check for password 72 | const isMatch = await user.mathPassword(password) 73 | if (!isMatch) { 74 | c.set.status = 401 75 | throw new Error('Invalid email or password!') 76 | } 77 | 78 | // Generate token 79 | const accessToken = await jwt.sign({ 80 | data: { id: user._id, isAdmin: user.isAdmin }, 81 | }) 82 | 83 | // Return success response 84 | return { 85 | status: c.set.status, 86 | success: true, 87 | data: { accessToken }, 88 | message: 'User logged in successfully', 89 | } 90 | } 91 | 92 | /** 93 | * @api [GET] /users 94 | * @description: Get all users 95 | * @action admin 96 | */ 97 | export const getUsers = async (c: Context) => { 98 | const users = await User.find().select('-password') 99 | 100 | // Check for users 101 | if (!users || users.length === 0) { 102 | c.set.status = 404 103 | throw new Error('No users found!') 104 | } 105 | 106 | // Return success response 107 | return { 108 | status: c.set.status, 109 | success: true, 110 | data: users, 111 | message: 'Users fetched successfully', 112 | } 113 | } 114 | 115 | /** 116 | * @api [GET] /users/:id 117 | * @description: Get a single user 118 | * @action admin 119 | */ 120 | export const getUser = async (c: Context<{ params: { id: string } }>) => { 121 | if (c.params && !c.params?.id) { 122 | c.set.status = 400 123 | throw new Error('No id provided') 124 | } 125 | 126 | return `Get user with id ${c.params.id}` 127 | } 128 | 129 | /** 130 | * @api [GET] /users/profile 131 | * @description: Get user profile 132 | * @action private 133 | */ 134 | export const getUserProfile = async (c: Context) => { 135 | // Get user id from token 136 | let token, userId 137 | if (c.headers.authorization && c.headers.authorization.startsWith('Bearer')) { 138 | token = c.headers.authorization.split(' ')[1] 139 | const decoded = await jwt.verify(token) 140 | userId = decoded.id 141 | } 142 | 143 | // Check for user 144 | const user = await User.findById(userId).select('-password') 145 | 146 | if (!user) { 147 | c.set.status = 404 148 | throw new Error('User not found!') 149 | } 150 | 151 | return { 152 | status: c.set.status, 153 | success: true, 154 | data: user, 155 | message: 'Profile fetched successfully', 156 | } 157 | } 158 | 159 | /** 160 | * @api [PUT] /users/:id 161 | * @description: Update a single user 162 | * @action public 163 | */ 164 | export const updateUser = async (c: Context<{ params: { id: string } }>) => { 165 | if (c.params && !c.params?.id) { 166 | c.set.status = 400 167 | throw new Error('No id provided') 168 | } 169 | 170 | // Check for body 171 | if (!c.body) throw new Error('No body provided') 172 | 173 | const { name, email, password } = c.body as UpdateBody 174 | 175 | // Update user 176 | const user = await User.findById(c.params.id) 177 | if (!user) { 178 | c.set.status = 404 179 | throw new Error('User not found!') 180 | } 181 | user.name = name || user.name 182 | user.email = email || user.email 183 | user.password = password || user.password 184 | const updatedUser = await user.save() 185 | 186 | if (!updatedUser) { 187 | c.set.status = 400 188 | throw new Error('Invalid user data!') 189 | } 190 | 191 | // Return success response 192 | return { 193 | status: c.set.status, 194 | success: true, 195 | data: updatedUser, 196 | message: 'User updated successfully', 197 | } 198 | } 199 | 200 | /** 201 | * @api [DELETE] /users/:id 202 | * @description: Delete a single user 203 | * @action public 204 | */ 205 | export const deleteUser = async (c: Context<{ params: { id: string } }>) => { 206 | if (c.params && !c.params?.id) { 207 | c.set.status = 400 208 | throw new Error('No id provided') 209 | } 210 | 211 | return `Delete user with id ${c.params.id}` 212 | } 213 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from 'elysia' 2 | // 3 | import { userRoutes } from './routes' 4 | import { error, logger } from './middlewares' 5 | import { connectDB } from './config' 6 | 7 | // Create Elysia instance 8 | const app = new Elysia() 9 | 10 | // Config MongoDB 11 | connectDB() 12 | 13 | // Middlewares 14 | app.use(logger()) 15 | app.use(error()) 16 | 17 | // Root Routes 18 | app.get('/', () => 'Welcome to our API') 19 | 20 | // User Routes [api/v1/users] 21 | app.use(userRoutes) 22 | 23 | // Start the server 24 | app.listen(Bun.env.PORT || 9000) 25 | 26 | console.log( 27 | `🚀 Server is running at ${app.server?.hostname}:${app.server?.port}` 28 | ) 29 | -------------------------------------------------------------------------------- /middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'elysia' 2 | import { User } from '~/models' 3 | import { jwt } from '~/utils' 4 | 5 | /** 6 | * @name auth 7 | * @description Middleware to protect routes with JWT 8 | */ 9 | export const auth: any = async (c: Context) => { 10 | let token 11 | if (c.headers.authorization && c.headers.authorization.startsWith('Bearer')) { 12 | try { 13 | token = c.headers.authorization.split(' ')[1] 14 | const decoded = await jwt.verify(token) 15 | const user = await User.findById(decoded.id) 16 | 17 | c.request.headers.set('userId', user?._id.toString()) 18 | c.request.headers.set('isAdmin', user?.isAdmin ? 'true' : 'false') 19 | } catch (error) { 20 | c.set.status = 401 21 | throw new Error('Not authorized, Invalid token!') 22 | } 23 | } 24 | 25 | if (!token) { 26 | c.set.status = 401 27 | throw new Error('Not authorized, No token found!') 28 | } 29 | } 30 | 31 | /** 32 | * @name admin 33 | * @description Middleware to protect routes with JWT and protect routes for admin only 34 | */ 35 | export const admin: any = async (c: Context) => { 36 | await auth(c) 37 | 38 | const isAdmin = c.request.headers.get('isAdmin') 39 | 40 | if (!isAdmin || isAdmin === 'false') { 41 | c.set.status = 401 42 | throw new Error('Not authorized as an Admin!') 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /middlewares/error.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | 3 | export const error = () => 4 | new Elysia().onError((c) => { 5 | if (c.code === 'NOT_FOUND') { 6 | c.set.status = 501 7 | return { 8 | success: false, 9 | status: 501, 10 | message: `Not Found - [${c.request.method}]:[${c.path}]`, 11 | stack: process.env.NODE_ENV === 'production' ? null : c.error?.stack, 12 | } 13 | } 14 | 15 | c.set.status = c.set.status || 500 16 | return { 17 | success: false, 18 | status: c.set.status, 19 | message: c.error?.message, 20 | stack: process.env.NODE_ENV === 'production' ? null : c.error?.stack, 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger' 2 | export * from './error' 3 | export * from './auth' 4 | -------------------------------------------------------------------------------- /middlewares/logger.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | import * as pc from 'picocolors' 3 | import process from 'process' 4 | 5 | export const logger = () => 6 | new Elysia() 7 | .onRequest((ctx) => { 8 | ctx.store = { ...ctx.store, beforeTime: process.hrtime.bigint() } 9 | }) 10 | .onBeforeHandle((ctx) => { 11 | ctx.store = { ...ctx.store, beforeTime: process.hrtime.bigint() } 12 | }) 13 | .onAfterHandle(({ request, store }) => { 14 | const logStr: string[] = [] 15 | if (request.headers.get('X-Forwarded-For')) { 16 | logStr.push(`[${pc.cyan(request.headers.get('X-Forwarded-For'))}]`) 17 | } 18 | 19 | logStr.push(methodString(request.method)) 20 | 21 | const path = (url: string) => `[[${url}]]` 22 | 23 | logStr.push(path(new URL(request.url).pathname)) 24 | const beforeTime: bigint = (store as any).beforeTime 25 | 26 | logStr.push(durationString(beforeTime)) 27 | 28 | console.log(logStr.join(': ')) 29 | }) 30 | .onError(({ request, error, store }) => { 31 | const logStr: string[] = [] 32 | 33 | logStr.push(pc.red(methodString(request.method))) 34 | 35 | logStr.push(new URL(request.url).pathname) 36 | 37 | logStr.push(pc.red('Error')) 38 | 39 | if ('status' in error) { 40 | logStr.push(String(error.status)) 41 | } 42 | 43 | logStr.push(error.message) 44 | const beforeTime: bigint = (store as any).beforeTime 45 | 46 | logStr.push(durationString(beforeTime)) 47 | 48 | console.log(logStr.join(' - ')) 49 | }) 50 | 51 | const durationString = (beforeTime: bigint): string => { 52 | const now = process.hrtime.bigint() 53 | const timeDifference = now - beforeTime 54 | const nanoseconds = Number(timeDifference) 55 | 56 | const durationInMicroseconds = (nanoseconds / 1e3).toFixed(0) // Convert to microseconds 57 | const durationInMilliseconds = (nanoseconds / 1e6).toFixed(0) // Convert to milliseconds 58 | let timeMessage: string = '' 59 | 60 | if (nanoseconds >= 1e9) { 61 | const seconds = (nanoseconds / 1e9).toFixed(2) 62 | timeMessage = `[${seconds}s]` 63 | } else if (nanoseconds >= 1e6) { 64 | timeMessage = `[${durationInMilliseconds}ms]` 65 | } else if (nanoseconds >= 1e3) { 66 | timeMessage = `[${durationInMicroseconds}µs]` 67 | } else { 68 | timeMessage = `[${nanoseconds}ns]` 69 | } 70 | 71 | return pc.cyan(timeMessage) 72 | } 73 | 74 | const methodString = (method: string): string => { 75 | const maxMethodLength = 9 76 | const methodStr = (m: string) => m.padEnd(maxMethodLength, ' ') 77 | 78 | switch (method) { 79 | case 'GET': 80 | return pc.bgCyan(methodStr('[GET]')) 81 | 82 | case 'POST': 83 | return pc.bgYellow(methodStr('[POST]')) 84 | 85 | case 'PUT': 86 | return pc.bgBlue(methodStr('[PUT]')) 87 | 88 | case 'DELETE': 89 | return pc.bgRed(methodStr('[DELETE]')) 90 | 91 | case 'PATCH': 92 | return pc.bgGreen(methodStr('[PATCH]')) 93 | 94 | case 'OPTIONS': 95 | return pc.bgWhite(methodStr('[OPTIONS]')) 96 | 97 | case 'HEAD': 98 | return pc.bgMagenta(methodStr('[HEAD]')) 99 | 100 | default: 101 | return method 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /models/index.ts: -------------------------------------------------------------------------------- 1 | export { default as User } from './userModel' 2 | -------------------------------------------------------------------------------- /models/userModel.ts: -------------------------------------------------------------------------------- 1 | import { Document, Schema, model } from 'mongoose' 2 | 3 | interface User { 4 | name: string 5 | email: string 6 | password: string 7 | isAdmin: boolean 8 | } 9 | 10 | interface UserDoc extends User, Document { 11 | mathPassword: (pass: string) => Promise 12 | } 13 | 14 | const userSchema = new Schema( 15 | { 16 | name: { type: String, required: true }, 17 | email: { type: String, required: true, unique: true }, 18 | password: { type: String, required: true }, 19 | isAdmin: { type: Boolean, required: true, default: false }, 20 | }, 21 | { 22 | timestamps: true, 23 | } 24 | ) 25 | 26 | // Match user entered password to hashed password in database 27 | userSchema.methods.mathPassword = async function (enteredPassword: string) { 28 | return Bun.password.verifySync(enteredPassword, this.password) 29 | } 30 | 31 | // Hash password with Bun 32 | userSchema.pre('save', async function (next) { 33 | if (!this.isModified('password')) { 34 | next() 35 | } 36 | 37 | // use bcrypt 38 | this.password = await Bun.password.hash(this.password, { 39 | algorithm: 'bcrypt', 40 | cost: 4, // number between 4-31 41 | }) 42 | }) 43 | 44 | const User = model('User', userSchema) 45 | export default User 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elysia-api-starter", 3 | "author": { 4 | "name": "Mehedi Hasan", 5 | "email": "admin@promehedi.com", 6 | "url": "https://promehedi.com" 7 | }, 8 | "version": "1.0.0", 9 | "scripts": { 10 | "start": "bun run index.ts", 11 | "dev": "bun run --watch index.ts" 12 | }, 13 | "dependencies": { 14 | "elysia": "^0.7.16", 15 | "jose": "^4.15.4", 16 | "mongoose": "^7.4.5", 17 | "picocolors": "^1.0.0" 18 | }, 19 | "devDependencies": { 20 | "bun-types": "latest" 21 | }, 22 | "module": "index.ts" 23 | } 24 | -------------------------------------------------------------------------------- /routes/index.ts: -------------------------------------------------------------------------------- 1 | export { default as userRoutes } from './userRoutes' 2 | -------------------------------------------------------------------------------- /routes/userRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { 3 | createUser, 4 | deleteUser, 5 | getUser, 6 | getUserProfile, 7 | getUsers, 8 | loginUser, 9 | updateUser, 10 | } from '~/controllers' 11 | import { admin, auth } from '~/middlewares' 12 | 13 | const userRoutes = (app: Elysia) => { 14 | app.group('/api/v1/users', (app) => 15 | // Create a new user 16 | app 17 | .post('/', createUser, { 18 | body: t.Object({ 19 | name: t.String(), 20 | email: t.String(), 21 | password: t.String(), 22 | isAdmin: t.Optional(t.Boolean()), 23 | }), 24 | type: 'json', 25 | }) 26 | 27 | // Login a user 28 | .post('/login', loginUser, { 29 | body: t.Object({ 30 | email: t.String(), 31 | password: t.String(), 32 | }), 33 | type: 'json', 34 | }) 35 | 36 | // Get all users 37 | .get('/', getUsers, { 38 | beforeHandle: (c) => admin(c), 39 | }) 40 | 41 | // Get a single user 42 | .get('/:id', getUser, { 43 | beforeHandle: (c) => auth(c), 44 | }) 45 | 46 | // Get user profile 47 | .get('/profile', getUserProfile, { 48 | beforeHandle: (c) => auth(c), 49 | }) 50 | 51 | // Update a single user 52 | .put('/:id', updateUser, { 53 | beforeHandle: (c) => admin(c), 54 | }) 55 | 56 | // Delete a single user 57 | .delete('/:id', deleteUser, { 58 | beforeHandle: (c) => admin(c), 59 | }) 60 | ) 61 | } 62 | 63 | export default userRoutes as any 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "ES2022", 5 | "moduleResolution": "node", 6 | "types": ["bun-types"], 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "noImplicitAny": true, 12 | "baseUrl": "./", 13 | "paths": { 14 | "~/*": ["./*"] 15 | } 16 | }, 17 | "exclude": ["node_modules", "dist"] 18 | } 19 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | import { jwt } from '@elysiajs/jwt' 4 | import { Context } from 'elysia' 5 | 6 | declare global { 7 | type RegBody = { 8 | name: string 9 | email: string 10 | password: string 11 | isAdmin?: boolean 12 | } 13 | 14 | type LoginBody = { 15 | email: string 16 | password: string 17 | } 18 | 19 | type UpdateBody = {} & Partial 20 | } 21 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt' 2 | -------------------------------------------------------------------------------- /utils/jwt.ts: -------------------------------------------------------------------------------- 1 | import * as jose from 'jose' 2 | 3 | const secret = new TextEncoder().encode(Bun.env.JWT_SECRET || 'secret') 4 | 5 | type JWT = { 6 | data: jose.JWTPayload 7 | exp?: string 8 | } 9 | 10 | export const sign = async ({ data, exp = '7d' }: JWT) => 11 | await new jose.SignJWT(data) 12 | .setProtectedHeader({ alg: 'HS256' }) 13 | .setIssuedAt() 14 | .setExpirationTime(exp) 15 | .sign(secret) 16 | 17 | export const verify = async (jwt: string) => 18 | (await jose.jwtVerify(jwt, secret)).payload 19 | 20 | export const jwt = { 21 | sign, 22 | verify, 23 | } 24 | --------------------------------------------------------------------------------