First Look At ElysiaJS

Building Modern APIs in the new Bun Runtime Environment

Nishant Aanjaney Jalan
CodeX
6 min readMar 9, 2024

--

Apologies for the absence from Medium for quite a while. I have a got a series of articles, tutorials, reflections and code planned out and I can’t wait to share my journey with you through the series

Recently, I had a major breakthrough as a front-end developer. I decided that I should spend more time with back-end development. I read this tweet fairly recently and it got me thinking what new technologies are there that makes Ben Holmes comment this.

After researching over the entire Internet (just Reddit.com) and listening to people’s opinions, I deduce that developers still love Express. Express has given a lot to the internet and it is still one of the most stable libraries; but I, personally, wanted to change and give other technologies a shot. That’s when I came across:

Bun

Source: Corey Butler’s Medium Post on Bun

Oh great! That’s what I need. Another JavaScript… wait, runtime environment?

Yep, it’s not a library or a framework; it’s a runtime environment. In 2018, Ryan Dahl invented Deno to fix Node but I don’t think it took off that well.

On 8th September 2023, Bun released their first stable version. Many JavaScript developers are trying this new runtime environment and it is looking quite promising. Famous Front-end frameworks like Next, Nuxt and SvelteKit are providing options to use Bun. I looked at Bun libraries to build APIs and I decided to learn:

ElysiaJS

Source: git@elysiajs/elysia

Note: As of writing this article, ElysiaJS is not stable. On NPM, version v0.8.17 is marked as latest, but release candidates v1.0.0-rc.11 are currently under development.

ElysiaJS claims itself to be an “Ergonomic Framework” that provides end-to-end type safety. After experimenting around with this language, I find it quite intuitive. It has a rich API, great plugin management and a robust validation API to ensure that what you expect is exactly what you get.

Installing Elysia

In this article, I will assume that you have already installed Bun. You can create a new Elysia application with:

bun create elysia hello-server
cd hello-server

If you have an existing application, you could install Elysia with

bun add elysia

And setup the necessary scripts in your package.json file detailed here.

First look at ElysiaJS

You can spin up a very minimal server in a few lines of code:

/* src/index.ts */
import { Elysia } from 'elysia';

const app = new Elysia()
.onStart(() => console.log("The server has started!"))
.get('/', () => "Hello Server")
.listen(8080);

You can run this server and test it with:

$ bun dev
The server has started!

$ curl http://localhost:8080/
Hello Server

You can group and create more routes by creating instances and using them in your main application.

/* src/index.ts */
import { Elysia } from 'elysia';

const age = new Elysia()
.get('/age', () => "I don't know your age")
.post('/age', ({ body: { age } }) => `Hi, you are ${age} years old`);

const app = new Elysia()
.onStart(() => console.log("The server has started!"))
.get('/', () => "Hello Server")
.use(age)
.listen(8080);

Every Elysia instance is a plugin of some sort that is an easy plug into the main application.

Feel free to check out and experiment with my repository that uses Elysia and Nuxt. This repository will be the basis of my series.

Type Safety

Most frameworks do what Elysia does. Why is it different? It is the type safety and validation. Consider the age example from above, the age is of type any. It is not type safe.

/* src/index.ts */
import { Elysia, t } from 'elysia';

const age = new Elysia()
.get('/age', () => "I don't know your age")
.post('/age', ({ body: { age } }) => `Hi, you are ${age} years old`, {
body: t.Object({
age: t.Number()
})
});

const app = new Elysia()
.onStart(() => console.log("The server has started!"))
.get('/', () => "Hello Server")
.use(age)
.listen(8080);

Let us run this server again with bun dev and use curl to test it

$ curl -v -H "Content-Type: application/json" \
> -X POST \
> -d '{"age": "20"}' \
> http://localhost:8080/age
{
"type": "body",
"at": "age",
"message": "Expected number",
"expected": {
"age": 0
},
"found": {
"age": "20"
},
"errors": [
{
"type": 41,
"schema": {
"type": "number"
},
"path": "/age",
"value": "20",
"message": "Expected number"
}
]
}

ElysiaJS throws an error when you sent a string as the age instead of a number. As a server-side developer, I did not have to worry about manually validating body data. If you view the headers of the response, you will see it returns HTTP/1.1 400 Bad Request. The curl will succeed if you pass an integer.

$ curl -v -H "Content-Type: application/json" \
> -X POST \
> -d '{"age": 20}' \
> http://localhost:8080/age
Hi, you are 20 years old

ElysiaJS/Eden

This is an extension to the Type-Safety heading. When testing the API Endpoints, we were using curl. As a developer, you might use Postman, or wget, but they are not type-safe. You could make mistakes when making requests that results in the undefined behaviour.

Front-end applications are more prone to making type errors when making API Requests. tRPC was a great attempt at providing this feature; however, its major drawback was the project’s adaptability to implement it. Eden solves this issue brilliantly, to be talked about more in future articles of this series.

Coming back to Eden, let’s write some unit tests to check if the routes behave the way they should. Firstly, we need to export the application type from index.ts.

/* src/index.ts */
import { Elysia, t } from 'elysia';

const age = new Elysia()
.get('/age', () => "I don't know your age")
.post('/age', ({ body: { age } }) => `Hi, you are ${age} years old`, {
body: t.Object({
age: t.Number()
})
});

const app = new Elysia()
.onStart(() => console.log("The server has started!"))
.get('/', () => "Hello Server")
.use(age)
.listen(8080);

export type App = typeof app // <- export the type

Secondly, we can write our unit tests. You would need to install bun add @elysiajs/eden as a dependency.

/* test/route.test.ts */
import { describe, expect, it } from 'bun:test';
import { edenTreaty } from '@elysiajs/eden';
import { type App } from '../src';

const BASE_URL = "http://localhost:8080";
const app = edenTreaty<App>(BASE_URL);

describe("Age API", () => {
it("GET / works as expected", async () => {
const { data, status } = await app.get();

expect(data).toBe("Hello Server");
expect(status).toBe(200);
});

describe("/age routes", () => {
it("GET /age returns IDK", async () => {
const { data, status } = await app.age.get();

expect(data).toBe("I don't know your age");
expect(status).toBe(200);
});

it("POST /age with number returns OK", async () => {
const body = { age: 20 };
const { data, status } = await app.age.post(body);

expect(data).toBe(`Hi, you are ${body.age} years old`);
expect(status).toBe(200);
});

it("POST /age with string returns error", async () => {
const body = { age: "20" };
const { status, error } = await app.age.post(body);

expect(status).toBe(400);
expect(error).toBeTruthy();
});

it("POST /age with wrong key returns error", async () => {
const body = { myAge: "20" };
const { status, error } = await app.age.post(body);

expect(status).toBe(400);
expect(error).toBeTruthy();
});
})
});

Given the routes we have, the type returned from edenTreaty are mapped in the following:

GET /              -> app.get()
GET /age -> app.age.get()
POST /age -> app.age.post(body)

GET /api/books -> app.api.books.get()
GET /api/books/:id -> app.api.books[id].get()
POST /auth/login -> app.auth.login.post(body)

While running the server on http://localhost:8080, test the application

$ bun test
bun test v1.0.30 (1424a196)

test/route.test.ts:
✓ Age API > /age routes > GET /age returns IDK [3.70ms]
✓ Age API > /age routes > POST /age with number returns OK [1.35ms]
✓ Age API > /age routes > POST /age with string returns error [5.12ms]
✓ Age API > /age routes > POST /age with wrong key returns error [0.94ms]
✓ Age API > GET / works as expected [0.59ms]

5 pass
0 fail
10 expect() calls
Ran 5 tests across 1 files. [43.00ms]

Eden is a great tool to get E2E Type Safety that you can use in your Front-end application, or when testing your server.

Conclusion

I am afraid that this article is overflowing. Sadly, I cannot continue writing this article explaining a whole bunch of features that Elysia has to offer. You can check out their docs to learn more here. The team behind Elysia are doing a great job and I hope this library has a great future. I can’t wait for their first stable release.

I hope you enjoyed reading my article and learned something. Thank you!

Want to connect?

My GitHub profile.
My Portfolio website.

--

--

Nishant Aanjaney Jalan
CodeX
Editor for

Undergraduate Student | CS and Math Teacher | Android & Full-Stack Developer | Oracle Certified Java Programmer | https://cybercoder-naj.github.io