End-To-End Type Safety With Next.js and Elysia.js
Greetings internet!
Today, I want to talk about how to create a full-stack application with Next.js and Elysia.js, featuring end-to-end type safety. We’ll tackle this task with the help of Eden, using Next.js v15. Let’s dive in!
If you’re not familiar with Elysia.js, I highly recommend taking a look at the official Elysia.js documentation.
As always, let’s create a Nextjs v15 application. You may check instructions here.
If you want to upgrade a specific project to Nextjs v15, just use this command below
bun add next@rc react@rc react-dom@rc eslint-config-next@rc
Let’s implement Elysiajs and Eden to our project:
bun add elysia @elysiajs/eden
And we are ready to go! Let’s take a look to folder structure of our app:
I’ve decided to create a folder called “elysia” for defining my controllers. This folder will be dedicated to my backend code. Now, it’s time to create our Elysia app!
I’ll create a file named index.ts
inside the "elysia" folder. This will serve as the root file for our Elysia app.
// elysia/index.ts
import { Elysia, t } from 'elysia'
export const elysiaApp = new Elysia({ prefix: '/api' })
export type TElysiaApp = typeof elysiaApp
Now we need to configure our client, which we’ll use for API calls. Let’s create a client.ts
file in the same location.
// elysia/client.ts
import type { TElysiaApp } from '@/elysia'
import { treaty } from '@elysiajs/eden'
// If you are not using Next.js v15^, you may want to set revalidate value to 0 due to default caching mechanics.
// export const elysia = treaty<TElysiaApp>('localhost:3000',{
// fetch: {
// next:{revalidate:0}
// },
// })
const url = process.env.URL_DOMAIN ?? "localhost:3000"
export const elysia = treaty<TElysiaApp>(url)
To work with Elysia.js instead of Next.js API routes, we need to make some minor configurations in our API endpoints. Let’s create a route.ts
file inside the app/api/[[...slugs]]
directory.
// app/api/[[...slugs]]
import { elysiaApp } from "@/elysia"
export const GET = elysiaApp.handle
export const POST = elysiaApp.handle
And we are ready to go! Let’s create our message controller
// elysia/controllers/message.ts
import Elysia, { t } from "elysia";
export const messageController = new Elysia({ prefix: '/message' })
.get('/', () => 'Hello From Elysia 🦊')
.get('/:message', ({ params }) => `Your Message: ${params.message} 🦊`, { params: t.Object({ message: t.String() }) })
.post('/', ({ body }) => body, {
body: t.Object({
name: t.String()
})
})
Time for updating our root elysia index.ts
file:
// elysia/index.ts
import { Elysia, t } from 'elysia'
import { messageController } from './controllers/message'
export const elysiaApp = new Elysia({ prefix: '/api' }).use(messageController).onError(({ code, error }) => {
console.log(code)
return new Response(JSON.stringify({ error: error.toString() }), { status: 500 })
})
export type TElysiaApp = typeof elysiaApp
And we are done! Now we can use beautiful syntax of elysiajs and eden ✨
// app/page.tsx
import { getWelcomingMessage } from "@/data-fetch-layer/message";
import { elysia } from "@/elysia/client";
export default async function Home() {
const { data, error } = await elysia.api.message.index.get()
// You may wrap it with "server actions"
// const {data, error} = await getWelcomingMessage()
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<p className="text-4xl font-bold text-black/80">{data}</p>
</main>
);
}
I won’t delve into the details of Elysia and Eden here; I just wanted to show you how to get started with them. Please refer to the official Elysia.js and Eden documentation for more information.
And of course, here is the template code that i provide in this article:
https://github.com/mertthesamael/nextjs-elysia-eden
Until Next Time! ❤️
My Website: www.merto.dev
Do not forget to drink at least 2 Liter of water per day ️