ElysiaJS Integration in Next.js
Discover the benefits of using ElysiaJS as your backend for Next.js applications. Explore various integration methods and typesafe api calls.
- Next.js
- ElysiaJS
- Prisma
Introduction
Hello everynyan! Today, we're diving into the world of full-stack web development using Next.js and Elysia.js, two powerful frameworks that streamline frontend and backend development, respectively.
Installation
- Create a new Next.js project, you can use my template by running this command:
npx create-t3-app
- Install dependencies
npm install bun add -d prisma
pnpm add bun add -d prisma
yarn add bun add -d prisma
bun add bun add -d prisma
bun add @prisma/client elysia @elysiajs/eden superjson @tanstack/react-query
- Create a new model in
prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id String @id @default(cuid())
content String
createdAt DateTime @default(now())
}
Note: you can run your postgresql database with docker by this command
docker run --name yuki-db -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres
cp .env.example .env
# .env
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/next-elysia"
If you want to add other variables, remember to add it to
env.js
to validate it
Next, add this script to your package.json
{
"scripts": {
"db:push": "prisma db push",
"db:studio": "prisma studio",
"postinstall": "prisma generate"
}
}
Then, push schema to your database
bun db:push
Backend Integration
- Create a Prisma service
To connect to the database, we need to create a Prisma service. This service will be used to interact with the database.
import { PrismaClient } from '@prisma/client'
import { env } from '@/env'
const createPrismaClient = () =>
new PrismaClient({
log:
env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})
const globalForPrisma = globalThis as unknown as {
prisma: ReturnType<typeof createPrismaClient> | undefined
}
export const db = globalForPrisma.prisma ?? createPrismaClient()
if (env.NODE_ENV !== 'production') globalForPrisma.prisma = db
- Create a configuration file for ElysiaJS
import type { ElysiaConfig } from 'elysia'
import Elysia from 'elysia'
import { db } from '@/server/db'
/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API.
*
* These allow you to access things when processing a request, like the database, the session, etc.
*
* This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
* wrap this and provides the required context.
*
* @see https://trpc.io/docs/server/context
*/
export const createElysiaContext = new Elysia()
.derive(async () => {
return { ctx: { db, session } }
})
.decorate('ctx', { db })
.as('plugin')
/**
* 2. INITIALIZATION
*
* This is where the elysia API is initialized, connecting the context and transformer. We also parse
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
* errors on the backend.
*/
export const elysia = <P extends string, S extends boolean>(
options?: ElysiaConfig<P, S>,
) => new Elysia(options).use(createElysiaContext)
/**
* Middleware for timing procedure execution and adding an artificial delay in development.
*
* You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
* network latency that would occur in production but not in local development.
*/
const timmingMiddleware = new Elysia()
.state({ start: 0 })
.onBeforeHandle(({ store }) => (store.start = Date.now()))
.onAfterHandle(({ path, store: { start } }) =>
console.log(`[Elysia] ${path} took ${Date.now() - start}ms to execute`),
)
.as('plugin')
/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*
* These are the pieces you use to build your elysia API. You should import these a lot in the
* "/src/server/api/routers" directory.
*/
export const createElysiaRouter = <P extends string, S extends boolean>(
options?: ElysiaConfig<P, S>,
) => elysia(options).use(timmingMiddleware)
- Create a new API endpoint
In this blog post, we will create a simple API endpoint to fetch latest posts and create a new post in the database.
import { t } from 'elysia'
import { createElysiaRouter } from '@/server/api/elysia'
export const postRouter = createElysiaRouter({ prefix: '/post' })
.get('/getLatestPost', async ({ ctx }) => {
const post = await ctx.db.post.findFirst({
orderBy: { createdAt: 'desc' },
})
return post ?? null
})
// .onBeforeHandle(({ ctx, error }) => {
// if (!ctx.session) return error('Unauthorized')
// })
.post(
'/createPost',
async ({ ctx, body }) => {
return ctx.db.post.create({ data: { content: body.content } })
},
{
body: t.Object({ content: t.String() }),
},
)
Then, add this route to your server
import type {
InferTreatyQueryInput,
InferTreatyQueryOutput,
} from '@ap0nia/eden-react-query'
import { treaty } from '@elysiajs/eden'
import { elysia } from '@/server/api/elysia'
import { postRouter } from '@/server/api/routers/post'
/**
* This is the primary router for your server.
*
* All routers added in /api/routers should be manually added here.
*/
const baseAppRouter = elysia({ prefix: '' })
// .use(edenPlugin({ batch: true, transformer: SuperJSON })) // make error
.use(postRouter)
const appRouter = elysia({ prefix: '/api/elysia' }).use(baseAppRouter)
// export type definition of API
type AppRouter = typeof baseAppRouter
/**
* Create a server-side caller for the tRPC API.
* @example
* const elysia = createCaller(createContext);
* const res = await elysia.post.all();
* ^? Post[]
*/
const createCaller = treaty(appRouter)
/**
* Inference helpers for input types
* @example
* type PostByIdInput = RouterInputs['post']['byId']
* ^? { id: number }
**/
// @ts-expect-error - lgtm
type RouterInputs = InferTreatyQueryInput<AppRouter>
/**
* Inference helpers for output types
* @example
* type AllPostsOutput = RouterOutputs['post']['all']
* ^? Post[]
**/
// @ts-expect-error - lgtm
type RouterOutputs = InferTreatyQueryOutput<AppRouter>
export { appRouter, createCaller }
export type { AppRouter, RouterInputs, RouterOutputs }
- Add the API endpoint to your Next.js application
import { appRouter } from '@/server/api/root'
const handler = appRouter.handle
export { handler as GET, handler as POST, handler as PUT, handler as DELETE }
API Integration
In this section, we will create a function to interact with the API endpoints we created earlier on both the server and client sides. In client side, we will use @tanstack/react-query
to cache the data.
- Create a server-side caller
import 'server-only'
import { createCaller } from '@/server/api/root'
export const api = createCaller.api.elysia
- Create a client-side caller
First, create a query client configuration for @tanstack/react-query
import { defaultShouldDehydrateQuery, QueryClient } from '@tanstack/react-query'
import SuperJSON from 'superjson'
export const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
dehydrate: {
serializeData: SuperJSON.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
},
hydrate: {
deserializeData: SuperJSON.deserialize,
},
},
})
Then, create a client-side caller
'use client'
import type { QueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import {
createEdenTreatyReactQuery,
httpBatchLink,
} from '@ap0nia/eden-react-query'
import { QueryClientProvider } from '@tanstack/react-query'
import type { AppRouter } from '@/server/api/root'
import { createQueryClient } from '@/lib/elysia/query-client'
import { getBaseUrl } from '@/lib/utils'
let clientQueryClientSingleton: QueryClient | undefined = undefined
const getQueryClient = () => {
if (typeof window === 'undefined') {
// Server: always make a new query client
return createQueryClient()
}
// Browser: use singleton pattern to keep the same query client
return (clientQueryClientSingleton ??= createQueryClient())
}
// @ts-expect-error - lgtm
export const api = createEdenTreatyReactQuery<AppRouter>({
abortOnUnmount: true,
})
export const ElysiaReactProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const queryClient = getQueryClient()
const [elysiaClient] = useState(() =>
api.createClient({
links: [
// @ts-expect-error - lgtm
httpBatchLink({
domain: getBaseUrl() + '/api/elysia',
}),
],
}),
)
return (
<QueryClientProvider client={queryClient}>
<api.Provider client={elysiaClient} queryClient={queryClient}>
{children}
</api.Provider>
</QueryClientProvider>
)
}
- Frontend Integration
Now, we can use the api
object to interact with the API endpoints we created earlier. Here's an example of how to fetch the latest post and create a new post.
'use client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { api } from '@/lib/elysia/react'
export const Post: React.FC = () => {
const [latestPost, { refetch }] =
api.post.getLatestPost.get.useSuspenseQuery()
const createPost = api.post.createPost.post.useMutation({
onSuccess: () => refetch(),
})
return (
<div className="mt-4 w-full max-w-(--breakpoint-sm)">
<span className="text-lg">
Latest post: {latestPost?.content ?? 'No posts'}
</span>
<form
className="flex w-full gap-4"
onSubmit={(e) => {
e.preventDefault()
createPost.mutate({
content: String(new FormData(e.currentTarget).get('content')),
})
e.currentTarget.reset()
}}
>
<Input
name="content"
placeholder="Post's content"
disabled={createPost.isPending}
/>
<Button disabled={createPost.isPending}>Post</Button>
</form>
</div>
)
}
Conclusion
In this blog post, we explored the benefits of using ElysiaJS as the backend for Next.js applications. We learned how to create a Prisma service, configure ElysiaJS, and create API endpoints to interact with the database. We also created a function to interact with the API endpoints on both the server and client sides. By following these steps, you can build powerful full-stack web applications with ease.
I hope you found this blog post helpful. If you have any questions or feedback, feel free to leave a comment below. Thank you for reading!
Repository: create-elynext-app
References
Build your own form
Learn how to build your own form with React and usng Standard Schema to validate the form.
Understanding Server Actions in Next.js
Learn how to improve your Next.js application's performance and user experience by utilizing server actions. This guide provides clear explanations and practical examples.