Discover the benefits of using ElysiaJS as your backend for Next.js applications. Explore various integration methods and typesafe api calls.
Next.jsElysiaJSPrisma
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
bun add -d prisma
bun add @prisma/client elysia @elysiajs/eden superjson @tanstack/react-query
Create a new model in prisma/schema.prisma
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
To connect to the database, we need to create a Prisma service. This service will be used to interact with the database.
server/db.ts
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
server/api/elysia.ts
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 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 APItype 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 - lgtmtype RouterInputs = InferTreatyQueryInput<AppRouter>/** * Inference helpers for output types * @example * type AllPostsOutput = RouterOutputs['post']['all'] * ^? Post[] **/// @ts-expect-error - lgtmtype RouterOutputs = InferTreatyQueryOutput<AppRouter>export { appRouter, createCaller }export type { AppRouter, RouterInputs, RouterOutputs }
Add the API endpoint to your Next.js application
app/api/elysia/[[...slug]]/route.ts
import { appRouter } from '@/server/api/root'const handler = appRouter.handleexport { 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
lib/elysia/server.ts
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
lib/elysia/query-client.ts
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
lib/elysia/react.tsx
'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 = undefinedconst 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 - lgtmexport 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.
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!