Basic ElysiaJS

Initiate Elysia project:

bun create elysia --project-name

Once the project created, there will be index.ts that would be the entry point of the app.

On the package.json file, there’s a script json. The dev is the script that will run the app as it watches the index.ts file under the src directory.

This is the index.ts file.

import { Elysia } from "elysia";

// variable app is a instance from Elysia constructor
// .get is method, a route from Elysia that has 2 arguments. first argument is the Path, second is the Handler/Controller
const app = new Elysia().get("/", () => "Hello Elysia").listen(3000);

console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);

Note: Elysia using chaining method to keep the type-safety feature active.

Routing

This is what the basic routing looks like on the index.ts file:

import { Elysia } from "elysia";

const app = new Elysia()
  .get("/", () => "Hello Elysia")
  .get("/users", () => "Users data")
  .get("/books", () => "Books data")
  .get("/products/:id", (ctx) => {
      // get params using context
    const { id } = ctx.params;
    return { message: `You're accessing ${id} endpoint` };
  });
  .listen(3000);

console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);

But we can make a route directory under the src directory to keep the code clear. For example, after creating route directory, we can create a new file called appRouter.ts and put the routing there.

import Elysia from "elysia";

// DONT FORGET to export it
export const appRouter = new Elysia()
  .get("/", () => "Hello Elysia")
  .get("/users", () => "Users data")
  .get("/books", () => "Books data");

Now the index.ts file looks like this:

import { Elysia } from "elysia";
import { appRouter } from "./routes/appRouter";

// .use(nameOfTheFile)
const app = new Elysia().use(appRouter).listen(3000);

console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);

As you can see, we need to add the .use() method to connect with the appRouter.ts file. And now the index.ts file looks cleaner.

Prefix for Organizing Routes

We can also add prefix for our routing. All we can do is adding the prefix inside the Elysia argument.

import Elysia from "elysia";

// {prefix: "/api/v1"}
export const appRouter = new Elysia({ prefix: "/api/v1" })
  .get("/", () => "Hello Elysia")
  .get("/users", () => "Users data")
  .get("/books", () => "Books data");

Now we can’t access the localhost:3000 as it would throw NOT_FOUND error. The route is now on the localhost:3000/api/v1 and all the routes are autumatically under the prefix.

JSON Response

As of now, we’re only returning text i.e “Hello Elysia” when we’re on the root path. Since most of the API responses are JSON object, we can achieve that by returning object for the second argument.

import Elysia from "elysia";

export const appRouter = new Elysia({ prefix: "/api/v1" })
  .get("/", () => "Hello Elysia")
  .get("/users", () => {
    return { message: "Users data" };
  })
  .get("/books", () => {
    return { message: "Books data" };
  });

When we check the localhost:3000/api/v1/users we will see JSON object returned instead of just a text.

// http://localhost:3000/api/v1/books

{
  "message": "Books data"
}

HTML/XML Response

Other than JSON & plain text, we can also return HTML/XML response. But first we need to add plugin by running this command:

bun add @elysiajs/html

Then we need to configure the tsconfig.json file and activate these:

{
....
 "jsx": "react"
 "jsxFactory": "Html.createElement"
 "jsxFragmentFactory": "Html.Fragment"
}

In order to returning Fragment or HTML, we need to convert te appRouter.ts file into appRouter.tsx. After that we can return HTML format to the browser.

import Elysia from "elysia";
// import this otherwise it throws Error
import { Html } from "@elysiajs/html";

export const appRouter = new Elysia({ prefix: "/api/v1" })
  .get("/", () => "Hello Elysia")
  .get("/users", () => {
    return { message: "Users data" };
  })
  // return H1 tag on the browser
  .get("/books", () => <h1>Books Data</h1>);

Handling Request Body

First we need to create POST method using .post() on the appRouter.tsx file. Here’s what it looks like:

import { Html } from "@elysiajs/html";
import Elysia from "elysia";

export const appRouter = new Elysia({ prefix: "/api/v1" })
  ....
  .post("/books", ({ body }) => {
    console.log(body);
    return "handling body";
  });

Test it using ThunderClient extension and body will be logged on the terminal. Here’s the request body I sent from ThunderClient:

{
  "name": "JSON Book",
  "description": "This is the description",
  "author": "JSON Mraz"
}

The body has "name", "description", "author".

But at this point, we don’t know the type of the body and it’s still unknown. We can’t get the body.name or body.description or body.author .

We can refactor it as the code below:

import { Html } from "@elysiajs/html";
import Elysia from "elysia";

export const appRouter = new Elysia({ prefix: "/api/v1" })
  ...
  .post("/books", ({ body }) => {
    const { name, description, author } = body as { name: string; description: string; author: string };
    console.log(name);
    console.log(description);
    console.log(author);
    return "handling body";
  });

The code above solved to identify the type-safety of the body. But it’s verbose as we need to add more type when the body has another variable.

Schema Guard

This is a feature from Elysia to help us indentify what type of property on the context, including the body.

To add the Schema Guard, we can add a new argument as the code below:

import { Html } from "@elysiajs/html";
// destructure the Elysia and add 't'
import { Elysia, t } from "elysia";

export const appRouter = new Elysia({ prefix: "/api/v1" })
  ...
  .post(
    "/books",
    ({ body }) => {
      const { name, description, author } = body;

      return "handling body";
    },
    // Schema Guard
    {
      body: t.Object({
        name: t.String(),
        description: t.String(),
        author: t.String(),
      }),
    }
  );

The t imported from Elysia help to determine the type of the body. As of now, when we hover the body there will be information about the type of the body that now has name: string, description: string, author: string .

The Schema Guard will restrict the data. So the data sent from the client has to be the same as the Schema Guard.

Headers Request

There would be a time when we need to protect our routes using Bearer Token. And we can protect it byusing the Authorization.

This is the basic code:

import { Html } from "@elysiajs/html";
import { Elysia, t } from "elysia";

export const appRouter = new Elysia({ prefix: "/api/v1" })
  ...
  .post("/books", ({ headers }) => {
    console.log(headers);
    return "Handling headers";
  });

On the ThunderClient, we need to add the Headers with Authorization and the value of ‘Bearer Token’. And when the POST method is triggered, we can see the log and there’s object with authorization property and its value is a string ‘Bearer Token’.

To get the authorization property we can create another Schema Guard:

import { Html } from "@elysiajs/html";
import { Elysia, t } from "elysia";

export const appRouter = new Elysia({ prefix: "/api/v1" })
  ...
  .post(
    "/books",
    ({ headers }) => {
      console.log(headers);
      return "Handling headers";
    },
    // Schema Guard for headers
    {
      headers: t.Object({
        authorization: t.String(),
      }),
    }
  );

Now we get the type-safety for headers inside console.log. authorization

And then if we check with ThunderClient, but this time we remove the Authorization with its value from the Headers, it will throw error with status 422 because it expects Authorization.

Query

To add query, we just need to change the context to query like this:

import { Html } from "@elysiajs/html";
import { Elysia, t } from "elysia";

export const appRouter = new Elysia({ prefix: "/api/v1" })
  ...
  .post("/books", ({ query }) => {
    console.log(query);
    return "Handling query";
  });

When we try it using ThunderClient with this query:

http://localhost:3000/api/v1/books?limit=10&category=comedy

The terminal will display this:

// terminal
{
  limit: "10",
  category: "comedy",
}

Hooks

Hooks is just a function that will be executed when there’s a trigger.

  • Global

Hooks that will run when there’s any kind of trigger request — in any endpoint.

export const appRouter = new Elysia({ prefix: "/api/v1" })
  .get("/", () => "Hello Elysia")
  // Global hooks
  .onBeforeHandle(() => {
    console.log("onBeforeHandle hook");
  })
  // original request
  .get("/users", () => {
    console.log("/users");
    return { message: "Users data" };
  })
  // other original request
  .get("/books", () => <div>Books Data</div>);

Just like the nature of Javascript, the code is executed from top to bottom. The onBeforeHandle would be executed before-handle-the-request.

When onBeforeHandle triggered we can see the result of console.log on the terminal. First is the from the global hook and second is from the original request (get method).

In real world, we can utilize onBeforeHandle hook to validate a request.

A hook can also return a value. For example in this case, the onBeforeHandle can return a value. But when there’s a return, the original request will not be executed.

export const appRouter = new Elysia({ prefix: "/api/v1" })
  .get("/", () => "Hello Elysia")
  // Global hooks
  .onBeforeHandle(() => {
    console.log("onBeforeHandle hook");
    // return, the code stops here
    return "from onBeforeHandle hook";
  })
  // this one would be ignored
  .get("/users", () => {
    console.log("/users");
    return { message: "Users data" };
  })
  .get("/books", () => <div>Books Data</div>);

Since the onBeforeHandle now has a return, the rest of the code would be ignored. So Javascript.

  • Local

Hooks that will run when there’s certain request — where the Local Hooks defined.

export const appRouter = new Elysia({ prefix: "/api/v1" })
  .get("/", () => "Hello Elysia")
  // Global Hooks
  .onBeforeHandle(() => {
    console.log("onBeforeHandle Global Hooks");
  })
  .get(
    "/users",
    () => {
      console.log("/users");
      return { message: "Users data" };
    },
    // Local Hooks
    {
      beforeHandle: () => {
        console.log("onBeforeHandle Local Hooks");
      },
    }
  );

// Result: Global hooks run first, then the Local hooks

In practical, Local Hooks used to validate a request in an endpoint.