I was playing around with Qwik. The most insanely fast web framework out there. Yes, the most.
And this is mainly because of the philosophy behind Qwik and its creator. Which you can read about here. An excellent read. If you ever liked Angular like me. Same creator.
Qwik apps are instant because there is nothing to do on application startup if the framework is resumable. No hydration, no islands. Just code splitting and lazy-loading components, functions, server functions, and even lazy-loading styles.
So of course we want an instant web app on the edge, right?
So I wanted to build the entire stack for a saas idea my girlfriend and I want to execute, and obviously, I want to put it at the edge!
In the next 8 min, I will show you the small details that could be really challenging if this is your first time with Cloudflare, Vite, Qwik, Prisma or even TypeScript. 📚
And I will tell you how to solve them, too. Enjoy.
Let's analyze the potential problems you'll face.
When working with Prisma at the edge, in this case Cloudflare which, by the way, published some new products and beta releases this month, and even alpha previews of their new database called D1, for example. Or even their new TCP adapter which now allows you to connect to your traditional database just like the old days. Learn more about it here.
But if you don't want to lose the value that the workers offer to your product and most important: your customers.
Then you want to be at the edge, and not just that! You want to use a CloudDB (HTTP) and you want to be resumable. #NeverHydrateAgain.
That's why we want to use Qwik, which is a very different framework in comparison with the most popular Next, Remix, or the smart SvelteKit.
Qwik allows you to not do hydration, they argue that hydration is a hack to recover the APP_STATE
and FRAMEWORK_STATE
by eagerly executing the app code in the browser. This involves:
downloading component code
executing component code
recovering the
WHAT
(APP_STATE
andFRAMEWORK_STATE
) andWHERE
to get event handler closureattaching
WHAT
(the event handler closure) toWHERE
(a DOM element)
All the explanations about WHAT
, APP_STATE
etc. can be found here.
Which DB should we use?
Last but not least: The Database. What is the best database? Wait, be careful, we can make a better question: Which database is the best, for the edge, specifically for Cloudflare?
Not an easy answer, right? That's why they are building a brand-new native SQL DB (D1).
But we need a production-ready db for our saas apps today, with the ability to use HTTPS communication instead of TCP. There are not so many HTTPS DBs out there yet, but one of the most interesting is Vitess, the open-source database technology that was invented at YouTube in 2010 to solve the scaling issues they faced with their massive MySQL database. The same DB that PlanetScale uses underneath.
I just randomly chose (So many options 🥶) I know I can do some SQL on mysql2.js
in a very frictionless way. But hey!, don't forget about type safety and Schema management. Nevertheless, the pick is:
MongoDBAtlas
+ PrismaProxy
+ PrismaClient
Cool ha? I also like to work with this group of nice APIs.
If all you have is a hammer, everything looks like a nail.
Well, we already know this won't be easy, but if we achieve the goal, this could be blazingly fast and amazingly powerful. So the trade-off seems to be pretty good.
Prepare the weekend! or a couple of nights 👊🏼 🍕🎧📚
Finally, We are using Cloudflare because of their amazing Edge network, which may be one of the game changers on cloud computing and don't forget that they arrived at first. We want all the benefits of a CDN, with the power of computing code if necessary in a beastly fast way. ⚡️ Yes, we want it all. 😳
So in summary. I'm very sure we are about to deal with:
Because Qwik is a server framework, we need to wrap our functions and components inside $
PrismaClient and Vite SSR builder are not best friends.
MongoDB Atlas does not offer an HTTPS connection with the free plan.
A Free Prisma cloud account would be necessary to proxy the connection to your DB via HTTPS.
Since Prisma recognizes Cloudflare runtime as a Browser (they are very similar) will need to instantiate PrismaClient isolated.
Cloudflare Env variables could not be visible for the PrismaClient
And any other random bug that can possibly happen is about to happen today. 🪲
But that's ok. We were made for this. 🐝
Let's install
Here is the link to the repo, so you can go directly to the source and read (which is much faster than reading this post). But if you stay, I'd like to show you the details and explain'em.
You can start an empty project with the tool I used: Vite. Vite is super easy to use and config and is semantically beautiful and maybe Vite poses one of the most active, inclusive, and smart communities. And the tools are just amazingly powerful, it is also a good idea to standardize a bit, so if you have plans to build your own framework, please, do it on Vite.
npm create qwik@latest
This will run a CLI and will generate a brand-new Qwik+ViteSSR project.
We're also going to install some dev tools: npm i prisma --save-dev
And some others for the project: npm i @prisma/client
Also used in this project:
"devDependencies": {
"@builder.io/qwik": "^1.1.4",
"@builder.io/qwik-city": "^1.1.4",
"@types/eslint": "8.37.0",
"@types/node": "^20.1.4",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"autoprefixer": "^10.4.14",
"daisyui": "^2.51.6",
"eslint": "8.40.0",
"eslint-plugin-qwik": "^1.1.4",
"postcss": "^8.4.23",
"prettier": "2.8.8",
"prisma": "^4.14.1",
"tailwindcss": "^3.3.1",
"typescript": "5.0.4",
"undici": "5.22.1",
"vite": "4.3.5",
"vite-tsconfig-paths": "4.2.0",
"wrangler": "^2.20.0",
"tiny-invariant": "^1.3.1",
},
"dependencies": {
"@prisma/client": "^4.14.1",
"zod": "^3.21.4"
}
How much code can we avoid building or avoid delivering to the client, just by working full-stack? Isn't this nice?
Now the Prisma.Schema
We can pull our database info with Prisma, so we do not need to write a single character of the Schemas (one of the main reasons why I want to use Prisma as a dev-tool):
npx prisma init
This will create our schema file at prisma/shcema.prisma
let's add the following:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
directUrl = env("MONGO_ATLAS")
}
model User {
name String?
email String @unique
id String @id @default(auto()) @map("_id") @db.ObjectId
access_token String?
picture String?
provider String?
refresh_token String?
}
Not much happening here, but for the two URLs: url
and directURL
.
You don't really need directURL
but since Prisma's --data-proxy
command doesn't support db push
or db pull
(more about Prisma Data Proxy here) we need to add it, so we can pull/push from the regular connection while we are developing.
Not bad ha?
So, let's get that two env variables.
Prisma Data Proxy
Yep, a Prisma service. Am I putting so much faith in Prisma?
Once you've related your database link to a proxy and chosen the region, it's time to create a .env file.
PRISMA_PROXY=prisma://aws-us-east-psdfsdflknjsadf
MONGO_ATLAS=mongodb+srv://blissmoasdpojdfsñihasdf
Remember, the Prisma proxy is about to deal as an intermediary between your web app and a TCP connection to your Atlas DB. Not exactly a CloudDB, but we are on the way. 🔥
Prisma Client at the edge
We also need to use a special Prisma Client that knows how to deal with the edge: npx prisma generate --data-proxy
So, we've pulled our database and updated some Models. It's time to generate the client:
npx prisma db push
And then:
npx prisma generate --data-proxy
With this, we are now able to use the import { PrismaClient } from "@prisma/client/edge"
inside of our code.
👀 Do not forget to add
"postinstall": "prisma generate && prisma generate --data-proxy"
into yor scripts insidepackage.json
. This way Prisma generates at build time on deploy.
Let's connect and use the database.
I've created one function to pass the Cloudflare Env variables to the PrismaClient in order to avoid any potential Env problems on Cloudflare, which are really hard to address. Kinda hacky, but useful.
src/db/getDB.ts
import { PrismaClient } from "@prisma/client/edge";
export const getDB = (
env: QwikCityPlatform["env"] & { get: (arg0: string) => string | undefined }
) => {
return new PrismaClient({
datasources: {
db: {
url: env.get("DATABASE_URL"),
},
},
});
};
It is important to mention that the import of the regular client is different from the proxy one, this will come from: @prisma/client/edge
This is very important. Please verify if you don't have incorrect imports in your files. 🕵🏽♂️
We don't have too much worry about resources with HTTP in comparison to TCP. Not our responsibility anymore… Thanks Prisma? (Is a Prisma proxy something interesting to build? 🤓)
Well, the use of these functions looks like this:
// this's a server function on Qwik:
const createPrismaUser = server$(
async (data: UserType, prisma: PrismaClient): Promise<UserType> => {
return (await prisma.user.upsert({
where: { email: data.email },
create: data,
update: data,
})) as UserType;
}
);
Disclaimer: I'm very sure this is not the best approach. But I'm very new to Qwik as a lot of my generation of developers, I want to be up-to-date and follow the most exciting open source projects. JS is such a gorgeous and fecund community. Please be patient. And to yourself. 🤓
So, check how I pass the Prisma client to this function: const user = await createPrismaUser(data, getDB(request.env));
export const onGet: RequestHandler = async (request) => {
const url = new URL(request.url);
const code = url.searchParams.get("code");
if (!code) return;
const { access_token, token_type } = await getToken(code, request);
const data = await getNewUserData({ access_token, token_type });
const user = await createPrismaUser(data, getDB(request.env));
request.cookie.set("userId", String(user.id), {
maxAge: [7, "days"],
path: "/",
httpOnly: true,
sameSite: "strict",
});
};
createPrismaUser
will get, as a second parameter, the PrismaClient. It's ok to instantiate on every connection. We are dealing with HTTP connections, remember? that's what make any DB a CloudDB
.
This Qwik endpoints
are just lovely 💜
Vite - Prisma build issue
We've got it all in place, now we need to try to build and see what happens.
One of the main problems, at the building step, is Vite saying:
./prisma/client/index-browser
or .prisma/client/edge
.
Not a very fortunate decision, to save the generated client in a hidden folder inside node_modules
😔. So we need to alias it on Vite's config file:
vite.config.ts
export default defineConfig(() => {
return {
resolve:{
alias: {
".prisma/client/edge":"./node_modules/.prisma/client/edge.js"
}
},
plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
preview: {
headers: {
'Cache-Control': 'public, max-age=600',
},
}
};})
Every time you find .prisma/client/edge
you'll use ./node_modules/.prisma/client/edge.js
instead. That's what it does. Yeah, that's a good one, Vite! 🥳😎 Maybe subscribe as a way to say thank you? 😜
Finally, let's deploy
Don't forget to add your Env variables to the Cloudflare console at the settings tab > Environment variables of your project.
I created the project on Cloudflare via their wizard, where I connected my GitHub account and selected my repo.
That's it, you now have it all, I've nothing left to give you today. Come back tomorrow. Maybe one more thing: #NeverHydrateAgain
Abrazo. Bliss.