Skip to content

Conversation

@fabrv
Copy link
Contributor

@fabrv fabrv commented May 10, 2025

Hi again!

These are some basic bindings for Bun's built-in routing (Bun >1.2.3).

It has a very big limitation that you'd always have to define the methods for each route handler, so there is no easy way to make a wildcard fallback, but for starters it's pretty good.

Here's the code example from https://bun.sh/docs/api/http with the new bindings:

let apiStatus: Bun.routeHandlerObject = {
  get: async (_request, _server) => Response.make("OK")
}

let userId: Bun.routeHandlerObject = {
  get: async (request, _server) => {
    let id = request->Bun.BunRequest.params->Dict.get("id")->Option.getUnsafe

    Response.make(`Hello user ${id}`)
  }
}

let posts: Bun.routeHandlerObject = {
  get: async (_, _) => Response.make("List posts"),
  post: async (request, _server) => {
    let body = await request->Request.json

    switch body {
    | Object(data) => Response.makeWithJsonUnsafe(data->Dict.set("created", Boolean(true)))
    | _ => Response.make("Invalid body type", ~options={status: 400})
    }
  }
}

let redirect: Bun.routeHandlerObject = {
  get: async (_, _) => Response.makeRedirect("/blog/hello/world")
}

let server = Bun.serve({
  routes: Dict.fromArray([
    ("/api/status", apiStatus)
    ,("/users/:id", userId)
    ,("/api/posts", posts)
    ,("/blog/hello", redirect)
  ]),
  fetch: async (_request, _server) => Response.make("Not found", ~options={status: 404})
})

let port =
  server
  ->Bun.Server.port
  ->Int.toString

let hostName = server->Bun.Server.hostname

Console.log(`Server listening on http://${hostName}:${port}!`)

@neobats
Copy link

neobats commented May 13, 2025

Would love to see this merged! I was looking at writing one myself, and saw you'd already done it, @fabrv.

@fabrv
Copy link
Contributor Author

fabrv commented May 13, 2025

@zth could you take a look at this PR? :)

Copy link
Owner

@zth zth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thank you!

Route object limitation is fine honestly. However. it would be nice to support static responses, since they have such a perf benefit according to Bun.

One way to do that could be to leverage unboxed variants, while making Repsonse.t a private {} instead of abstract. Sample code and playground:

module Server = {
  type t
}

module Response = {
  // Make this `private {}` instead of abstract, so it can be used in unboxed variants with multiple payloads. Beware, the formatter has a bug where `private` is removed, so you'll need to save without formatting.
  type t = private {}
}

module BunRequest = {
  type t
  @get
  external params: t => Dict.t<string> = "params"
  @get
  external cookies: t => Iterator.t<(string, string)> = "cookies"
}

// Allow setting static or function
@unboxed
type routeHandlerForMethod =
  Static(Response.t) | Handler((BunRequest.t, Server.t) => promise<Response.t>)

type routeHandlerObject = {
  @as("DELETE") delete?: routeHandlerForMethod,
  @as("GET") get?: routeHandlerForMethod,
  @as("HEAD") head?: routeHandlerForMethod,
  @as("OPTIONS") options?: routeHandlerForMethod,
  @as("PATCH") patch?: routeHandlerForMethod,
  @as("POST") post?: routeHandlerForMethod,
  @as("PUT") put?: routeHandlerForMethod,
}

type routes = Dict.t<routeHandlerObject>

https://rescript-lang.org/try?version=v11.1.4&module=esmodule&code=LYewJgrgNgpgBAZRgJwG4rgXjgbwFBxwAuAngA7xF4C+eeokscASjAM5kgB2b82+hUhWJY4ZZAEtUAQyLwctWvXDR4AIQhdWARwjsiogcXKUCcAAIBzGFUIwAHnORdpUMdOTTgbAFwjMAHxwACISAMZEAHREADxsRJJclkHYAERkHl5sqWZWNmYOTi5uYSAgANYS7H4GgXAAkk6yIMjRMQAU8YmWADRwXRJJAJQpcKmlFVXZNHTmmgBGIPYwYHhC8MggEHIAEtJcYLDIAGItALI2ABbgWGYIRLLh7awc3LzRQ3AAPixbcu3tDRaGC6fTRPpINAoD5YILiEDACS8GIvTg8GDRAJDOjrOCbbYwPYHI4AeXmACsYBFDLlpGx2qlggBRAAyTIAKkzUp8wDBYHIAPx+fG7faHFCnZAXIjXMA9Wn01IAcQ53Lg1iIQrxf0JYqOkulsvlhHMdIZOyZAEFgmrLjBpGAtSLdcSJecruBjRYzakSQAFdn1EkAOQQapAZCIEjeTp1RPFJ3dMs9CoZfst7IAwjs1RkiGFLrGCfH9UmjanUn6SQh2bmQPEi6LXYmpR65RW-QBVWufMjbRsuhMGtvypS451sUShCJtZ0llBkykRAJ4IA

Your example would then be:

let server = Bun.serve({
  routes: Dict.fromArray([
    ("/api/status", Handler(apiStatus))
    ,("/users/:id", Handler(userId))
    ,("/api/posts", Handler(posts))
    ,("/blog/hello", Handler(redirect))
  ]),
  fetch: async (_request, _server) => Response.make("Not found", ~options={status: 404})
})

What do you think about that?

@fabrv
Copy link
Contributor Author

fabrv commented May 13, 2025

@zth That's great, I didn't knew that trick!

With that change we still need to name every method per route, but at least now we can have a static response for a method. The example would actually look like this:

let apiStatus: Bun.routeHandlerObject = {
  // here's where the variant is used
  get: Handler(async (_request, _server) => Response.make("OK"))
  post: Static(Response.make("This is static"))
}

let server = Bun.serve({
  routes: Dict.fromArray([
    ("/api/status", apiStatus)
  ])
})

For it to be a variant at the route level it would need to be something like this:

@unboxed
type routeHandler =
| Static(Response.t)
| Methods(routeHandlerObject)

type routes = Dict.t<routeHandler>

But obviously that's not possible because both Response.t and routeHandlerObject are object types.

I agree that the Route object limitation is fine and with the Static and Handler variants at the method level it's good enough.

btw, I updated the PR and ran rescript build

@neobats
Copy link

neobats commented May 13, 2025

@zth @fabrv y'all are amazing! This is far better than what I hacked together locally to try and get this working

@zth
Copy link
Owner

zth commented May 14, 2025

Awesome! Merging this and aiming to get a new release out within the coming days.

@neobats @fabrv would you be interested in doing more bindings work on rescript-bun? There's probably plenty of new things with no bindings yet that would be great to get in.

@zth zth merged commit f073e00 into zth:main May 14, 2025
1 check passed
This was referenced Jun 27, 2025
zth added a commit that referenced this pull request Jul 26, 2025
* note about 2.x

* fix: update depracated getWithDefault from Readme (#10)

* Add `routes` object in `serveOptions` (#11)

* feat: add router to serveOptions

* remove empty line

* change Promise.t to promise in router handler

* add unboxed variant type for static and handler method responses

* Basic Sqlite bindings (#12)

* changelog

* basic bindings for SQLite

* changelog

* chore: add changesets for release (#15)

* add unreleased changes as changesets

* try changing config

* permissions

* feat: bun redis bindings (#14)

* feat: bun redis bindings

* fix: resolve unit test

* fix: resolve playground tests

* Version Packages (#16)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* package lock

* 2.0.0-alpha.1 and rescript v12 support

* up readme

* Update to ReScript v12

* Run test

* Add changelog entry

* Revert shell test change

---------

Co-authored-by: Gabriel Nordeborn <gabbe.nord@gmail.com>
Co-authored-by: Fabrizio Delcompare <35388973+fabrv@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants