
In this blog post we are going to add authentication in Bun based REST API.
Bun
Bun is relatively new javascript runtime that is built on top of JavaScriptCore engine (used in apple safari) and Zig programming language. It has built in transpiler, bundler, test runner and npm-compatible package manager.
Elysia
Elysia is a fully type-safe web framework built on top of Bun having familier syntax like express.
Prisma
Prisma is a Nodejs and Typescript ORM that reduce the burden of writting pure SQL command to interact with database. You can use both SQL and NoSQL database with prisma.
In this post we are going to use Postgresql to store user data and we will use Prisma cli to initialize new postgresql database and apply schema migrations.
Prerequisite
Install Bun (https://bun.sh/docs/installation)
Setup Postgresql (https://www.postgresql.org/download/)
Lets create new elysia project using bun command line
bun create elysia auth
Now openauth
project in vscode
cdauthcode.
src/index.ts
importElysiafrom"elysia";import{auth}from"~modules/auth";import{cookie}from"@elysiajs/cookie";import{jwt}from"@elysiajs/jwt";constapp=newElysia().group("/api",(app)=>app.use(jwt({name:"jwt",secret:Bun.env.JWT_SECRET!,})).use(cookie()).use(auth)).listen(8080);console.log(`🦊 Elysia is running at${app.server?.hostname}:${app.server?.port}`);
First we created an instance ofElysia
then we addedjwt
andcookie
plugins provided by elysia. You can install both plugins using bun command.
bun add @elysiajs/cookie @elysiajs/jwt
cookie
plugin adds support for using cookie in Elysia handler andjwt
plugin adds support for using JWT in Elysia handler. Internally@elysiajs/jwt
usejose
(https://github.com/panva/jose).
We used grouping features of elysia which allows you to combine multiple prefixes into one.
Suppose that we have these routes having repeated prefix.
/api/auth/signup
/api/auth/login
/api/auth/logout
Instead we can group them with prefix/api/
.
Forjwt
plugin you can explicitly register the JWT function with a different name usingname
property.
You can access environment variable in Bun usingBun.env
. Create a dot file on top level.env.local
and addJWT_SECRET
.
.env.local
JWT_SECRET="itssecret"
Then you can useBun.env.JWT_SECRET
to accessJWT_SECRET
value available in env file. Because Bun is Node compatible so you can also useprocess.env.JWT_SECRET
.
In TypeScript, the exclamation mark (!) is known as the non-null assertion operator. It is used to assert that a value is not null or undefined.
We have registeredauth
module usingapp.use(auth)
so that we can keep our auth related handlers separate.
Next we are going to setup prisma.
Add Prisma CLI as a development dependency
bun add-d prisma
Next, set up your Prisma project by creating your Prisma schema file with the following command:
bunx prisma init
bunx
is similer tonpx
orpnpx
the primary purpose ofbunx
is to facilitate the execution of packages that are listed in thedependencies
ordevDependencies
section of a project'spackage.json
file. Instead of manually installing these packages globally or locally, you can usebunx
to run them directly.
Now create user schema insideprisma/schema.prisma
file
generator client { provider = "prisma-client-js"}datasource db { provider = "postgresql" url = env("DATABASE_URL")}model User { id String @id @default(uuid()) name String username String @unique email String @unique salt String hash String summary String? links Json? location Json? profileImage String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt}
Next we are going to apply migrations to createuser
table in our database.
bunx prisma db push
Migrations are changes to your database schema, such as creating tables, altering columns, or adding indexes.
Next we are going to add prisma client package to interact with database.
bun add @prisma/client
Inside/src/libs/prisma.ts
create instance of prisma client and export it.
import{PrismaClient}from"@prisma/client";exportconstprisma=newPrismaClient();
Inside/src/modules/auth/index.ts
add auth related handlers
import{Elysia,t}from"elysia";import{prisma}from"~libs/prisma";import{comparePassword,hashPassword,md5hash}from"~utils/bcrypt";import{isAuthenticated}from"~middlewares/auth";exportconstauth=(app:Elysia)=>app.group("/auth",(app)=>app.post("/signup",async({body,set})=>{const{email,name,password,username}=body;// validate duplicate email addressconstemailExists=awaitprisma.user.findUnique({where:{email,},select:{id:true,},});if(emailExists){set.status=400;return{success:false,data:null,message:"Email address already in use.",};}// validate duplicate usernameconstusernameExists=awaitprisma.user.findUnique({where:{username,},select:{id:true,},});if(usernameExists){set.status=400;return{success:false,data:null,message:"Someone already taken this username.",};}// handle passwordconst{hash,salt}=awaithashPassword(password);constemailHash=md5hash(email);constprofileImage=`https://www.gravatar.com/avatar/${emailHash}?d=identicon`;constnewUser=awaitprisma.user.create({data:{name,email,hash,salt,username,profileImage,},});return{success:true,message:"Account created",data:{user:newUser,},};},{body:t.Object({name:t.String(),email:t.String(),username:t.String(),password:t.String(),}),}).post("/login",async({body,set,jwt,setCookie})=>{const{username,password}=body;// verify email/usernameconstuser=awaitprisma.user.findFirst({where:{OR:[{email:username,},{username,},],},select:{id:true,hash:true,salt:true,},});if(!user){set.status=400;return{success:false,data:null,message:"Invalid credentials",};}// verify passwordconstmatch=awaitcomparePassword(password,user.salt,user.hash);if(!match){set.status=400;return{success:false,data:null,message:"Invalid credentials",};}// generate accessconstaccessToken=awaitjwt.sign({userId:user.id,});setCookie("access_token",accessToken,{maxAge:15*60,// 15 minutespath:"/",});return{success:true,data:null,message:"Account login successfully",};},{body:t.Object({username:t.String(),password:t.String(),}),}).use(isAuthenticated)// protected route.get("/me",({user})=>{return{success:true,message:"Fetch authenticated user details",data:{user,},};}));
Here we have grouped all handlers in/auth
prefix.
set
is used to set status code , headers or redirect for response.
Usingbody
we can parse request body data in our case this will be JSON request body.
We have used Elysia Schema to add validation for request body. Schema is used to define the strict type for the Elysia handler. Like in Login route we defined the structure of schema that we are going to receive from client in the third parameter ofapp.post()
. Here we have added schema validation forbody
but you can add schema validation for query, params, header etc...
Now create/src/utils/bcrypt.ts
and add following codes:
import{randomBytes,pbkdf2,createHash}from"node:crypto";asyncfunctionhashPassword(password:string):Promise<{hash:string;salt:string}>{constsalt=randomBytes(16).toString("hex");returnnewPromise((resolve,reject)=>{pbkdf2(password,salt,1000,64,"sha512",(error,derivedKey)=>{if(error){returnreject(error);}returnresolve({hash:derivedKey.toString("hex"),salt});});});}asyncfunctioncomparePassword(password:string,salt:string,hash:string):Promise<boolean>{returnnewPromise((resolve,reject)=>{pbkdf2(password,salt,1000,64,"sha512",(error,derivedKey)=>{if(error){returnreject(error);}returnresolve(hash===derivedKey.toString("hex"));});});}functionmd5hash(text:string){returncreateHash("md5").update(text).digest("hex");}export{hashPassword,comparePassword,md5hash};
We added utility function to hash plain password , compare password and generate md5 hash from strings usingnode:crypto
package.
/src/middlewares/auth.ts
import{Elysia}from"elysia";import{prisma}from"~libs";exportconstisAuthenticated=(app:Elysia)=>app.derive(async({cookie,jwt,set})=>{if(!cookie!.access_token){set.status=401;return{success:false,message:"Unauthorized",data:null,};}const{userId}=awaitjwt.verify(cookie!.access_token);if(!userId){set.status=401;return{success:false,message:"Unauthorized",data:null,};}constuser=awaitprisma.user.findUnique({where:{id:userId,},});if(!user){set.status=401;return{success:false,message:"Unauthorized",data:null,};}return{user,};});
derive allows you to customize Context based on existing Context. Here we have retureduser
from derive nowuser
will available in handlers context.
You can configuretsconfig.json
paths
directory to resolve non-relative module names.
"paths":{"~libs/*":["./src/libs/*"],"~modules/*":["./src/modules/*"],"~utils/*":["./src/utils/*"],"~middlewares/*":["./src/middlewares/*"]},
Lets start the server
bun run dev
🦊 Elysia is running at 0.0.0.0:8080
Top comments(8)

Seems like a fairly recent tutorial, however Prisma.ts tells me "@prisma/client" has no exported member called PrismaClient.
PS: The paths fix that you say to add topackage.json, should actually be added totsconfig.json
- Email
- LocationIndia
- EducationBCA
- Pronounshe/him
- WorkSoftware Engineer
- Joined
Thanks Chola , I have fixed

Can you please also provide source code for this?
- Email
- LocationIndia
- EducationBCA
- Pronounshe/him
- WorkSoftware Engineer
- Joined
This is the repo from where i have picked up auth part.
github.com/harshmangalam/elysia-bl...

I don't like the way we have to use ts-ignore to remove types if we have to move the code to other file have you find any solution for this
I am working on same
github.com/bhumit070/bun-drizzle
- Email
- LocationIndia
- EducationBCA
- Pronounshe/him
- WorkSoftware Engineer
- Joined
Thanks Samuel Levy, I have corrected this
For further actions, you may consider blocking this person and/orreporting abuse