Back to blogs
Blog
Writing in public
Longer posts — published here first, then cross-posted to Dev.to with a canonical URL back to this site.
Blog
Longer posts — published here first, then cross-posted to Dev.to with a canonical URL back to this site.
Back to blogs
Back to blogs
Postreact, vite, typescript, prisma
initialize the project final code Setup using Rakkasjs which is a full-stack vite react...
Date published

slides used in the presentation building the generic component In this example we'll use...

Why You Might Still Pick Next.js Over TanStack Start From a TanStack Start...

A complete theme management system for React applications with SSR support! ✨ 🌟...
Setup using Rakkasjs which is a full-stack vite react framework
1pnpm create rakkas-app my-rakkas-app
hattip
what is hattip? , it's what powers the backend for Rakkasjs, and allows you to write code that'll run in multiple runtimes and environments, like node, bun,deno, and Cloudflare workers ...
1pnpm install hattip @hattip/response @hattip/cookie http-status-codes devalue
trpc
What is trpc? , An easy way to write typesafe APIs in typescript, collocated backend, and frontend share types and give you a typesafe RPC API
1pnpm install @trpc/client @trpc/react-query @trpc/server superjson zod
prisma
This one is optional as tprc doesn't necessarily need it , but we're trying to recreate the t3-stack which in Nextjs in vite + react, you can use whatever javascript orm / data source
1pnpm install prisma
What is rakkasjs? , The closest thing to a Nextjs on Vite.
To get started we'll create the trpc API endpoint which will be a catch-all route with the all verb.
To create API routes in Rakkasjs we put it in the src/routes/api directory, A simple API route would look something like this
1// src/routes/api/index.api.ts2import { json } from "@hattip/response";3import { StatusCodes } from "http-status-codes";4import { RequestContext } from "rakkasjs";56export async function get(ctx: RequestContext) {7try {8 return json({ message: "welcome to rakkas root api" },{ status: StatusCodes.ACCEPTED });9 } catch (error) {10 return json(error, { status: StatusCodes.BAD_REQUEST }); }11}1213export async function post(ctx: RequestContext) {14 const body = await ctx.request.json();15 try {16 return json({body },{ status: StatusCodes.ACCEPTED });17 } catch (error) {18 return json(error,{ status: StatusCodes.BAD_REQUEST });19 }20}21
we're going to be doing alot of relative imports so let's setup tsconfig aliases to point @/ to src/
12{3 "compilerOptions": {4 "target": "es2020",5 "module": "ESNext",6 "esModuleInterop": true,7 "forceConsistentCasingInFileNames": true,8 "strict": true,9 "skipLibCheck": true,10 "moduleResolution": "Node",11 "resolveJsonModule": true,12 "jsx": "react-jsx",13 "baseUrl": ".",14 "types": ["vite/client"],151617 "paths": {18 "@/*": ["src/*"]19 }20212223 }24}25
Our trpc catch-all route
1// src/routes/api/trpc/[...trpc].api.ts2import { fetchRequestHandler } from '@trpc/server/adapters/fetch';3import { RequestContext } from 'rakkasjs';4import { createTRPCContext } from '@/server/trpc';5import { appRouter } from '@/server/routes/root';67export async function all(ctx: RequestContext){8 return fetchRequestHandler({9 endpoint: '/api/trpc',10 req: ctx.request,11 router: appRouter,12 createContext:createTRPCContext,13 });14}
The only difference between this and the nextjs create-t3-app is that we're using the fetchRequestHandler instead of the nextjs adapter
hattip like most of the other agnostic runtime servers relies on the standard fetch API.
We'll put all the trpc logic in the src/server directory.
first, we'll create the trpc context
1export function createTRPCContext({ req, resHeaders }: FetchCreateContextFnOptions) {2 const user = { name: req.headers.get('username') ?? 'anonymous' };3 // return createInnerTRPCContext({});4 return { req, resHeaders, user };5}6
then we init trpc
1const t = initTRPC.context<typeof createTRPCContext>().create({2 transformer: superjson,3 errorFormatter({ shape, error }) {4 return {5 ...shape,6 data: {7 ...shape.data,8 zodError:9 error.cause instanceof ZodError ? error.cause.flatten() : null,10 },11 };12 },13});
then we export the router and procedure
1export const createTRPCRouter = t.router;2export const router = t.router3export const publicProcedure = t.procedure;
Then we can now create the trpc router + it's endpoints
1// src/server/routes/root.ts2import { createTRPCRouter, publicProcedure } from '../trpc';3import { helloRouter } from './hello';4// to test using a REST API client use a . to access nested routes insteda of a slash5//ex: http://localhost:5173/api/trpc/welcome6//ex: http://localhost:5173/api/trpc/hello.wave78export const appRouter = createTRPCRouter({9 hello:helloRouter,10});1112// export type definition of API13export type AppRouter = typeof appRouter;14
1// src/server/routes/hello.ts2import { createTRPCRouter, publicProcedure } from "../trpc";3export const helloRouter = createTRPCRouter({4 hey: publicProcedure.query((opts) => {5 return `Hey there`;6 }),7 wave: publicProcedure.query(async(opts) => {8 return `🙋♀️`;9 }),1011})
With thhat done , our endppoints are ready to consume on the frontend
lets add some frontend dependancies
1pnpm install @tanstack/react-query react-toastify lucide-react @unpic/react tailwind-merge
Then we configure the trpc client in src/utils/trpc.ts
1// src/utils/trpc.ts2import type { AppRouter } from '@/server/routes/root';3import { createTRPCReact } from '@trpc/react-query';456export const trpc = createTRPCReact<AppRouter>();7
Then we create a trpc client for the provider
1// src/utils/client.ts2import { trpc } from "./trpc";3import { httpBatchLink } from "@trpc/react-query";4import superjson from "superjson";56const getBaseUrl = (url?:string) => {7 if (typeof window !== "undefined") return ""; // browser should use relative url8 const urlObj = new URL(url as string);9 if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url10 return urlObj.origin;11};121314export const trpcClient = (url?:string)=>{15 return trpc.createClient({16 links: [17 httpBatchLink({18 url:`${getBaseUrl(url)}/api/trpc`,19 }),20 ],21 transformer: superjson22 });23}24
Then we setup the stack react query provider
1// src/routes/layout.tsx2const [queryClient] = useState(() => new QueryClient());3...4return(5 <trpc.Provider client={trpcClient()} queryClient={queryClient}>6 <QueryClientProvider client={queryClient}>7 <section className="min-h-screen h-full w-full ">{children}</section>8 </QueryClientProvider>9 </trpc.Provider>10)
Rakkasjs has layouts , like the ones in nextjs app router which lets you wrap multiple pages with a commone layout , the one at the root src/routes/layout.tsx wraps the whole app so we can put our providers thereLet's add some tailwind for styling too official guide
try
1npx bonita add tailwind
or manually
1pnpm i -D tailwindcss postcss autoprefixer daisyui tailwindcss-animate tailwind-scrollbar tailwindcss-elevation prettier-plugin-tailwindcss
1npx tailwindcss init -p
Now add the tailwind config content paths
1/** @type {import('tailwindcss').Config} */2export default {3 content: [4 "./index.html",5 "./src/**/*.{js,ts,jsx,tsx}",6 ],7 theme: {8 extend: {},9 },10 plugins: [],11}
And include the base CSS in our layout
src/routes/index.css
1@tailwind base;2@tailwind components;3@tailwind utilities;
1// src/routes/layout.tsx2import ./index.css3
and now we consume our trpc endpoint
1// src/routes/index.page.tsx2import { trpc } from "@/utils/trpc";34export default function HomePage() {5 const query = trpc.hello.hey.useQuery();6 return (7 <main className="flex flex-col gap-2 items-center">8 <h1>Hello world!</h1>9 <h3>{query?.data}</h3>10 </main>11 );12}13
Taddah, Your app is now ready

let's add the Prisma parts, first thing we need is a schema and the env variables prisma/schema.prisma
1 // This is your Prisma schema file,2// learn more about it in the docs: https://pris.ly/d/prisma-schema34generator client {5 provider = "prisma-client-js"6}78generator zod {9 provider = "zod-prisma-types"10}1112datasource db {13 provider = "sqlite"14 url = env("DATABASE_URL")15}1617model User {18 id String @id @default(cuid())19 name String20 email String21 password String22 createdAt DateTime @default(now())23 updatedAt DateTime @updatedAt24}2526model Post {27 id String @id @default(cuid())28 createdAt DateTime @default(now())29 updatedAt DateTime @updatedAt3031 title String32 body String33}3435
1# .env2DATABASE_URL="file:./db.sqlite"
then we run the generate command
1npx prisma generate2npx prisma migrate dev
and now we create a helper function for the db
1// src/server/db.ts2import { PrismaClient } from "@prisma/client";3import { envs } from "@/utils/env";45const globalForPrisma = globalThis as unknown as {6 prisma: PrismaClient | undefined;7};89export const prisma =10 globalForPrisma.prisma ??11 new PrismaClient({12 log:envs.DEV_MODE ? ["query", "error", "warn"] : ["error"],13 // env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],14 });1516// if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;17if (envs.PROD_MODE) globalForPrisma.prisma = prisma;18
and we add prisma into our trpc context
1export function createTRPCContext({ req, resHeaders }: FetchCreateContextFnOptions) {2 const user = { name: req.headers.get('username') ?? 'anonymous' };3 // return createInnerTRPCContext({});4 return { req, resHeaders, user,prisma };5}
We can now use it in our trpc routes
1// src/server/posts.ts23import { createTRPCRouter, publicProcedure } from "../trpc";4import { z } from "zod";56export const PostSchema = z.object({7 id: z.string().cuid().optional(),8 createdAt: z.coerce.date().optional(),9 updatedAt: z.coerce.date().optional(),10 title: z.string(),11 body: z.string(),12})1314export type Post = z.infer<typeof PostSchema>1516export const postRouter = createTRPCRouter({17 // get the full list18 getFullList: publicProcedure.query(({ctx}) => {19 const posts = ctx.prisma.post.findMany();20 return posts;21 }),2223 create: publicProcedure24 .input(PostSchema)25 .mutation(({ctx, input}) => {26 const post = ctx.prisma.post.create({27 data: input,28 });29 return post;30 }),3132 update: publicProcedure33 .input(PostSchema)34 .mutation(({ctx, input}) => {35 const post = ctx.prisma.post.update({ where: {id: input.id},data: input,});36 return post;37 }),38 delete: publicProcedure39 .input(z.object({id: z.string()}))40 .mutation(({ctx, input}) => {41 const post = ctx.prisma.post.delete({ where: {id: input.id}});42 return post;43 })44})4546// src/server/root.ts47export const appRouter = createTRPCRouter({48 hello:helloRouter,49 posts:postRouter50});5152
We can now consume it in the frontend
1// src/routes/posts2 const query = trpc.posts.getFullList.useQuery();3 return (4 <main className="flex flex-col gap-2 items-center">5 <h1>Hello world!</h1>6 <h3>{query?.data.map(...)}</h3>7 </main>8 );9
That's all you need to get started, but we can optimize this further For starters, we're not taking full advantage of the SSR step to preload the data.
Rakkasjs hasit;s built-in useQuery and useServerSideQuery hooks to preload the data during the SSR step.
but since we're using tanstack react query we can use the integration guide the creator put out that lets you prefetch the data in the SSR step
we do this by adding Rakkasjs hoos hooks
client-entry.tsx : Code here will run on/wrap every page We can move the providers from the layout to here1/* eslint-disable no-var */2import { startClient } from "rakkasjs";3import { QueryClientProvider, QueryClient } from "@tanstack/react-query";4import { trpc } from "./utils/trpc";5import { trpcClient } from "./utils/client";678const queryClient = new QueryClient({9 defaultOptions: {10 queries: {11 suspense: true,12 staleTime: 100,13 refetchOnWindowFocus: false,14 refetchOnReconnect: false,15 },16 },17});1819function setQueryData(data: Record<string, unknown>) {20 for (const [key, value] of Object.entries(data)) {21 queryClient.setQueryData(JSON.parse(key), value, { updatedAt: Date.now() });22 }23}2425declare global {26 var $TQD: Record<string, unknown> | undefined;27 var $TQS: typeof setQueryData;28}2930// Insert data that was already streamed before this point31setQueryData(globalThis.$TQD ?? {});32// Delete the global variable so that it doesn't get serialized again33delete globalThis.$TQD;34// From now on, insert data directly35globalThis.$TQS = setQueryData;3637startClient({38 hooks: {39 wrapApp(app) {40 return (41 <trpc.Provider client={trpcClient()} queryClient={queryClient}>42 <QueryClientProvider client={queryClient}>{app}</QueryClientProvider>43 </trpc.Provider>44 );45 },46 },47});48
hattip-entry.tsx We also need to create server-side providers1import { RequestContext, createRequestHandler } from "rakkasjs";2import { cookie } from "@hattip/cookie";34import {5 QueryCache,6 QueryClient,7 QueryClientProvider,8} from "@tanstack/react-query";9import { uneval } from "devalue";10import { trpc } from "./utils/trpc";11import { trpcClient } from "./utils/client";1213declare module "rakkasjs" {14 interface ServerSideLocals {15 session: any;16 }17}1819type CreateRequestHandlerParams = Parameters<typeof createRequestHandler>;2021const attachSession = async (ctx: RequestContext) => {22 try {23 // ctx.locals.session = await getSession(24 // ctx.platform.request,25 // ctx.platform.response26 // );2728 ctx.locals.session = {29 user: "jeffery",30 };31 } catch (error) {32 throw new Error("Failed to attach session");33 }34};35const logger = async (ctx: RequestContext) => {36 try {37 console.log("========", ctx.ip, "=============");38 } catch (error) {39 throw new Error("Failed to attach session");40 }41};4243export default createRequestHandler({44 middleware: {45 // HatTip middleware to be injected46 // before the page routes handler.47 // @ts-expect-error48 beforePages: [cookie(), attachSession],49 // HatTip middleware to be injected50 // after the page routes handler but51 // before the API routes handler52 beforeApiRoutes: [logger],53 // HatTip middleware to be injected54 // after the API routes handler but55 // before the 404 handler56 beforeNotFound: [],57 },5859 createPageHooks(ctx) {60 let queries = Object.create(null);61 console.log("hattip ctx", ctx.request.url);62 return {63 wrapApp(app) {64 const queryCache = new QueryCache({65 onSuccess(data, query) {66 // Store newly fetched data67 queries[query.queryHash] = data;68 },69 });7071 const queryClient = new QueryClient({72 queryCache,73 defaultOptions: {74 queries: {75 suspense: true,76 staleTime: Infinity,77 refetchOnWindowFocus: false,78 refetchOnReconnect: false,79 },80 },81 });8283 return (84 <trpc.Provider85 client={trpcClient(ctx.request.url)}86 queryClient={queryClient}87 >88 <QueryClientProvider client={queryClient}>89 {app}90 </QueryClientProvider>91 </trpc.Provider>92 );93 },9495 emitToDocumentHead() {96 return `<script>$TQD=Object.create(null);$TQS=data=>Object.assign($TQD,data);</script>`;97 },9899 emitBeforeSsrChunk() {100 if (Object.keys(queries).length === 0) return "";101102 // Emit a script that calls the global $TQS function with the103 // newly fetched query data.104105 const queriesString = uneval(queries);106 queries = Object.create(null);107 return `<script>$TQS(${queriesString})</script>`;108 },109 };110 },111});112
src/common-hooks.tsx : is also an option but we won't use it hereThe hooks also allow you to define middleware and locals for things like sessions which can be shared between pages and API routes
That's it, there's also a CLI, simply run
1npx bonita create