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.
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.