Writing a simple RESTful TypeScript web service with Bun.js (2024)

Nazeel

Posted on

Writing a simple RESTful TypeScript web service with Bun.js (2) Writing a simple RESTful TypeScript web service with Bun.js (3) Writing a simple RESTful TypeScript web service with Bun.js (4) Writing a simple RESTful TypeScript web service with Bun.js (5) Writing a simple RESTful TypeScript web service with Bun.js (6)

#bunjs #webdev #restapi #typescript

Bun.js is a JavaScript runtime written in Zig and powered by JavaScriptCore (which used in WebKit). It has native support for TypeScript, its own test runner, package manager, bundler and much much more.

In this blog post, I will attempt to guide you through the process of writing a simple feature flag RESTful web service with Bun.js and zod.

Yes, you read that right. No other dependencies, frameworks or libraries would be required to get this up and running.

Prerequisites

Not much! Just some basic knowledge around RESTful services, JavaScript, TypeScript will do. I will try my best to explain each step as descriptively as possible.

Remember when I said no other dependencies? I lied... sorta. You also need an API client like cURL, Postman or Insomnia.

Setting up

First, head over to the Bun installation and get bun installed. Execute the following command to see if bun is installed:

➜ bun --version1.0.21

Awesome! We can now scaffold a simple bun project by running bun init command:

➜ bun initbun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quitpackage name (feature-flag-service): entry point (index.ts): Done! A package.json file was saved in the current directory. + index.ts + .gitignore + tsconfig.json (for editor auto-complete) + README.mdTo get started, run: bun run index.ts

Simple as that! Now let's install zod, of which more will be revealed further down this article.

➜ bun add zodbun add v1.0.21 (837cbd60) installed zod@3.22.4 1 package installed [130.00ms]

Amazing! Now let's create add a script that can start bun in hot reload mode so that we can have the latest version of our service without having to stop and start.

{ "name": "feature-flag-service", "module": "index.ts", "type": "module", "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5.0.0" }, "scripts": { "start": "bun --hot run index.ts" }, "dependencies": { "zod": "^3.22.4" }}

Run the start command like so:

➜ bun start$ bun --hot run index.tsHello via Bun!

Let's update the index.ts file and see if bun hot reloads the file:

console.log("Hello via Bun!")console.log("Initializing the feature flag service")

Save the file, and you should see that the terminal updates with both the console logs printed.

Hello via Bun!Initializing the feature flag service

With that, we have our setup completed!

Creating a HTTP server in Bun

To create and HTTP server in Bun, the Bun.serve function can be used. This takes in an object with many parameters, but we are interested in 2 of them. Namely fetch; which handles the request and port which designates the port at which to listen to.

Let's add that into the end of the index.ts file:

Bun.serve({ port: 8080, fetch(req) { return new Response("Bun!") },})

Give yourself a pat on the back because you just created a HTTP server! Save this and go to localhost:8080 to confirm. You should see a webpage that displays Bun!.

Writing a simple RESTful TypeScript web service with Bun.js (7)

Making it RESTful

If you inspect the response in the inspector, you will see that Bun is just wrapping whatever response we give pass to the Response constructor in a pre HTML tag.

Our service is going to be RESTful, so we should only send and receive in JSON. To make our server return a JSON, let's make some changes to the fetch function:

fetch(req) { return new Response( JSON.stringify({ message: 'Hello!' }), { headers: { 'Content-Type': 'application/json' } } )}

Now if you refresh the webpage, you can see that the response has changed into JSON and depending on your browser, it should render either the JSON view or the plaintext JSON.

Writing a simple RESTful TypeScript web service with Bun.js (8)

Feature flag service

A feature flag is a string key boolean value pair that determines if the key feature is enabled or not based on value. A feature flag service exposes endpoints that help you create/update and read feature flag values. These correspond to the PUT and GET HTTP verbs. The endpoint would be something like localhost:8080/flags.

Let's add some types at the top of the file to help us determine what type of call the client is trying to make.

// Typestype Path = '/flags'type Method = 'GET' | 'PUT'type ApiEndpoint = `${Method} ${Path}`

ApiEndpoint will be one of PUT /flags or GET /flags.

Routing

Let's update the fetch function to determine what type of call is coming through.

try { const url = new URL(req.url) const method = req.method const apiEndpoint: ApiEndpoint = `${method as Method} ${url.pathname as Path}` switch(apiEndpoint) { case 'PUT /flags': return new Response( JSON.stringify({ message: `You called PUT /flags` }), { headers: { 'Content-Type': 'application/json' }, status: 200 } ) case 'GET /flags': return new Response( JSON.stringify({ message: `You called GET /flags` }), { headers: { 'Content-Type': 'application/json' }, status: 200 } ) default: return new Response( JSON.stringify({ message: `You called ${apiEndpoint}, which I don't know how to handle!` }), { headers: { 'Content-Type': 'application/json' }, status: 404 } ) }} catch(err) { console.log(err) return new Response(JSON.stringify({ message: 'Internal Server Error' }), { headers: { 'Content-Type': 'application/json' }, status: 500})}

Pretty simple.

  1. We create a URL object by passing the request URL into the URL constructor. This gives us a handy way to access the URL, its params, and so on.
  2. Then, we extract the request method into the method variable, which can be one of the HTTP verbs.
  3. We then construct a string called apiEndpoint, concatenating the two values calculated above.
  4. A switch block that tests against apiEndpoint, which effectively is our service router at this point.
  5. Handlers for each route and method inside each case block.
  6. Wrap this all in a nice try-catch block to catch any runtime errors for the implementations to follow!

Okay, let's try to access the endpoint now with out API client of choice (I'm using Insomnia, because it's 1:24 AM as I type this).

GET /flags:
Writing a simple RESTful TypeScript web service with Bun.js (9)

PUT flags:
Writing a simple RESTful TypeScript web service with Bun.js (10)

DELETE flags:
Writing a simple RESTful TypeScript web service with Bun.js (11)

With that, we have created a service level router for all incoming requests. Now we can define each of our route functionalities more clearly...

... but before that, a bit of refactoring!

You may have noticed in the last code block, we blatantly violated the DRY principle in the form of all those 4 Response objects we created in each return statement. The pattern seems to be:

Response( JSON.stringify(someJSON), { headers: { 'Content-Type': 'application/json' }, status: someNumber })

We have to keep in mind:

  1. We always want to JSON stringify our response, this is to abide by the principle of RESTful services.
  2. We always want to have the Content-Type header as application/json, due to the same reason above.

We could create a class that wraps some of these repeated functionalities so it's easier to use. At the top of the file, below the types, add the following:

// Constantsconst responseHeaders = { headers: { 'Content-Type': 'application/json' } }// Custom classesclass CustomResponse extends Response { constructor(response: Record<any, any>, headerOverride?: Bun.ResponseInit) { super(JSON.stringify(response), {...responseHeaders, ...headerOverride}) }}

This class extends the response class and does the JSON.stringify and header override for us. Now we can replace the fetch code with the following:

try { const url = new URL(req.url) const method = req.method const apiEndpoint: ApiEndpoint = `${method as Method} ${url.pathname as Path}` switch(apiEndpoint) { case 'PUT /flags': return new CustomResponse({ message: `You called PUT /flags` }, { status: 200 }) case 'GET /flags': return new CustomResponse({ message: `You called GET /flags` }, { status: 200 }) default: return new CustomResponse({ message: `You called ${apiEndpoint}, which I don't know how to handle!` }, { status: 404 }) }} catch(err) { console.log(err) return new CustomResponse({ message: 'Internal Server Error' }, { status: 500 })}

Wow, so much cleaner and readable!

Implementing PUT /flags

When accepting a new flag (or updation to an existing flag) We would like the PUT payload to follow a certain shape:

{ key: string, value: boolean}

We could use something like Object.hasOwnProperty to verify this, then assert that each key is a typeof boolean or string. Or, we could use zod, a static type inference as well as a runtime validator for a request object.

At the top, add the following code:

import { z } from "zod"// Other typesconst FeatureFlag = z.object({ key: z.string().min(1), value: z.boolean()})type FeatureFlagType = z.infer<typeof FeatureFlag>

With this, we say that FeatureFlag is an object that has a key of type string with length of at least 1 and a value of type boolean. We can then infer the type into a static TS type by using z.infer.

Before we proceed, let's make our fetch function async so we can do a lot of the asynchronous tasks required to get the PUT /flags running.

Bun.serve({ port: 8080, async fetch(req) { // fetch implementation }})

Now we can flesh out the PUT route:

case 'PUT /flags': { const request = await req.json() as FeatureFlagType if (!FeatureFlag.safeParse(request).success) { return new CustomResponse({ message: 'Bad Request' }, { status: 400 }) } const featureFlagToInsert = { [request.key]: request.value } let updatedFeatureFlagInfo = featureFlagToInsert // write to file try { const featureFlagFile = Bun.file('feature_flags.json', { type: "application/json" }) if (await featureFlagFile.exists()) { const featureFlagObject = JSON.parse(await featureFlagFile.text()) updatedFeatureFlagInfo = { ...featureFlagObject, ...updatedFeatureFlagInfo } } Bun.write(featureFlagFile, JSON.stringify(updatedFeatureFlagInfo)) } catch(err) { console.log(err) return new CustomResponse({ message: 'Internal Server Error' }, { status: 500}) } return new CustomResponse({ message: 'Created' }, { status: 201})}

Whoa. Lots to unpack here, so let's go through it step by step.

  1. We first get the request body by awaiting the req.json().
  2. Using the power of almighty zod, we parse the object and verify that it is indeed conforming to our definition of the FeatureFlag zod validator. Note that we use safeParse instead of parse since the latter would throw an error and we'd have to handle that.
  3. We then change the format so that we can insert the feature flag into our global feature flag object. But where do we store it? How can it be persisted?
  4. Bun's file I/O comes swooping in to the rescue. Leveraging Bun.file which lazy loads an instance of a file, we create one.
  5. If this file exists, we fetch the text, parse it into JSON, and update the object.
  6. Finally, we write the updated object back into the file.

Let's see it in action:
Writing a simple RESTful TypeScript web service with Bun.js (12)

That wraps up the PUT implementation of feature flags.

Implementing GET /flags

The GET endpoint logic is pretty simple.

  1. We check the request URL search params for a key name. If it does not exist, we return with a 400 Bad Request response.
  2. If the feature flag file exists, we parse it and fetch the key from the feature flag object. If it does not, we simply return that the key is not registered.
  3. The fetched key is then converted into the FeatureFlagType type and returned.

So it'll look something like this:

case 'GET /flags': { const featureFlagFile = Bun.file('feature_flags.json', { type: "application/json" }) const key = url.searchParams.get('key') if (!key) { return new CustomResponse({ message: 'Key missing in request' }, { status: 400 }) } if (await featureFlagFile.exists()) { const featureFlagObject = JSON.parse(await featureFlagFile.text()) const value = featureFlagObject[key] if (value === undefined) { return new CustomResponse({ message: 'Key is not registered' }, { status: 404 }) } const response: FeatureFlagType = { key, value } return new CustomResponse(response, { status: 200}) } return new CustomResponse({ message: 'Key is not registered' }, { status: 404 })}

Now let's see it run!

Writing a simple RESTful TypeScript web service with Bun.js (13)

Voila! We can also fetch this information from the service now.

Conclusion

In conclusion, we have learned how to:

  1. Create a HTTP server in Bun.js
  2. Tweak it to be a RESTful service
  3. Create a service level router per method and endpoint
  4. Implement the functionality of the feature flag service
  5. Leverage Bun's File I/O to persist the feature flag information

Additional challenges

If you have been following along so far (you rock!) and still want to do more, I invite you to try to:

  1. Add a DELETE /flags endpoint which deletes the key provided.
  2. Replace the File I/O with an in-memory database to speed up the service.
  3. Add a simple authentication to the feature flag service.
  4. Bundle up this project using Bun.build and host it in a cloud provider (or any server, Raspberry Pie, etc) of your choice.

References:

  1. Bun Docs
  2. Zod Docs
  3. Sample Code
Writing a simple RESTful TypeScript web service with Bun.js (2024)
Top Articles
Latest Posts
Article information

Author: Jeremiah Abshire

Last Updated:

Views: 6090

Rating: 4.3 / 5 (54 voted)

Reviews: 85% of readers found this page helpful

Author information

Name: Jeremiah Abshire

Birthday: 1993-09-14

Address: Apt. 425 92748 Jannie Centers, Port Nikitaville, VT 82110

Phone: +8096210939894

Job: Lead Healthcare Manager

Hobby: Watching movies, Watching movies, Knapping, LARPing, Coffee roasting, Lacemaking, Gaming

Introduction: My name is Jeremiah Abshire, I am a outstanding, kind, clever, hilarious, curious, hilarious, outstanding person who loves writing and wants to share my knowledge and understanding with you.