├── .prettierignore
├── process-env-shim.js
├── public
└── favicon.ico
├── .gitignore
├── app
├── entry.client.tsx
├── entry.server.tsx
├── db.server.ts
├── root.tsx
└── routes
│ ├── index.tsx
│ └── users
│ └── $id.tsx
├── remix.env.d.ts
├── server.ts
├── remix.config.js
├── .editorconfig
├── wrangler.toml
├── prisma
└── schema.prisma
├── .prettierrc.json
├── tsconfig.json
├── .github
└── ISSUE_TEMPLATE
│ ├── config.yml
│ └── bug_report.yml
├── scripts
└── build.js
├── LICENSE
├── README.md
└── package.json
/.prettierignore:
--------------------------------------------------------------------------------
1 | build
2 | dist
3 | .mf
4 | .cache
5 |
--------------------------------------------------------------------------------
/process-env-shim.js:
--------------------------------------------------------------------------------
1 | export var process = {
2 | env: new Proxy(
3 | {},
4 | {}
5 | ),
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcomafessolli/remix-prisma-cloudflare-workers/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /public/build
6 |
7 | .env
8 | .mf
9 | .idea
10 | .vscode
11 |
--------------------------------------------------------------------------------
/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | import { hydrate } from 'react-dom'
2 | import { RemixBrowser } from '@remix-run/react'
3 |
4 | hydrate(, document)
5 |
--------------------------------------------------------------------------------
/remix.env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
--------------------------------------------------------------------------------
/server.ts:
--------------------------------------------------------------------------------
1 | import { createEventHandler } from "@remix-run/cloudflare-workers";
2 | import * as build from "@remix-run/dev/server-build";
3 |
4 | addEventListener(
5 | "fetch",
6 | createEventHandler({ build, mode: process.env.NODE_ENV })
7 | );
8 |
--------------------------------------------------------------------------------
/remix.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@remix-run/dev').AppConfig}
3 | */
4 |
5 | module.exports = {
6 | serverBuildTarget: "cloudflare-workers",
7 | server: "./server.ts",
8 | devServerBroadcastDelay: 1000,
9 | ignoredRouteFiles: ["**/.*"],
10 | };
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = false
12 | insert_final_newline = true
--------------------------------------------------------------------------------
/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "remix-cloudflare-workers"
2 | type = "javascript"
3 |
4 | workers_dev = true
5 |
6 | [site]
7 | bucket = "./public"
8 | entry-point = "."
9 |
10 | [build]
11 | command = "npm run build"
12 |
13 | [build.upload]
14 | format="service-worker"
15 |
16 | #[secrets]
17 | #DATABASE_URL=""
18 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "mongodb"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid()) @map("_id")
12 | email String @unique
13 | name String?
14 | }
15 |
16 | model Log {
17 | id String @id @default(cuid()) @map("_id")
18 | level Level
19 | message String
20 | meta Json
21 | }
22 |
23 | enum Level {
24 | Info
25 | Warn
26 | Error
27 | }
28 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSameLine": false,
4 | "bracketSpacing": true,
5 | "embeddedLanguageFormatting": "auto",
6 | "htmlWhitespaceSensitivity": "css",
7 | "insertPragma": false,
8 | "jsxSingleQuote": true,
9 | "printWidth": 80,
10 | "proseWrap": "preserve",
11 | "quoteProps": "as-needed",
12 | "requirePragma": false,
13 | "semi": false,
14 | "singleQuote": true,
15 | "tabWidth": 2,
16 | "trailingComma": "es5",
17 | "useTabs": false,
18 | "vueIndentScriptAndStyle": false
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules", "build-worker/**/*", "build/**/*"],
4 | "compilerOptions": {
5 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
6 | "esModuleInterop": true,
7 | "jsx": "react-jsx",
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "target": "ES2019",
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "baseUrl": ".",
14 | "paths": {
15 | "~/*": ["./app/*"]
16 | },
17 | "noEmit": true,
18 | "isolatedModules": true,
19 | "allowJs": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | import type { EntryContext } from "@remix-run/cloudflare";
2 | import { RemixServer } from "@remix-run/react";
3 | import { renderToString } from "react-dom/server";
4 |
5 | export default function handleRequest(
6 | request: Request,
7 | responseStatusCode: number,
8 | responseHeaders: Headers,
9 | remixContext: EntryContext
10 | ) {
11 | let markup = renderToString(
12 |
13 | );
14 |
15 | responseHeaders.set("Content-Type", "text/html");
16 |
17 | return new Response("" + markup, {
18 | status: responseStatusCode,
19 | headers: responseHeaders,
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/app/db.server.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client/edge";
2 |
3 | let db: PrismaClient;
4 |
5 | declare global {
6 | var __db__: PrismaClient;
7 | }
8 |
9 | // this is needed because in development we don't want to restart
10 | // the server with every change, but we want to make sure we don't
11 | // create a new connection to the DB with every change either.
12 | // in production we'll have a single connection to the DB.
13 | if (process.env.NODE_ENV === "production") {
14 | db = new PrismaClient();
15 | } else {
16 | if (!global.__db__) {
17 | global.__db__ = new PrismaClient();
18 | }
19 | db = global.__db__;
20 | db.$connect();
21 | }
22 |
23 | export { db };
24 |
--------------------------------------------------------------------------------
/app/root.tsx:
--------------------------------------------------------------------------------
1 | import type { MetaFunction } from "@remix-run/cloudflare";
2 | import {
3 | Links,
4 | LiveReload,
5 | Meta,
6 | Outlet,
7 | Scripts,
8 | ScrollRestoration,
9 | } from "@remix-run/react";
10 |
11 | export const meta: MetaFunction = () => ({
12 | charset: "utf-8",
13 | title: "New Remix App",
14 | viewport: "width=device-width,initial-scale=1",
15 | });
16 |
17 | export default function App() {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 🤔 Feature Requests & Questions
4 | url: https://github.com/marcomafessolli/remix-prisma-cloudflare-workers/discussions
5 | about: Please ask and answer questions here.
6 | - name: 💬 Remix Discord Channel
7 | url: https://rmx.as/discord
8 | about: Interact with other people using Remix 📀
9 | - name: 💬 New Updates (Twitter)
10 | url: https://twitter.com/remix_run
11 | about: Stay up to date with Remix news on twitter
12 | - name: 🍿 Remix YouTube Channel
13 | url: https://www.youtube.com/channel/UC_9cztXyAZCli9Cky6NWWwQ
14 | about: Are you a techlead or wanting to learn more about Remix in depth? Checkout the Remix YouTube Channel
15 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | var esbuild = require("esbuild");
2 | var NodeModules = require("@esbuild-plugins/node-modules-polyfill");
3 |
4 | var NodeModulesPolyfillPlugin = NodeModules.NodeModulesPolyfillPlugin;
5 |
6 | async function build() {
7 | var mode = process.env.NODE_ENV?.toLowerCase() ?? "development";
8 |
9 | await esbuild.build({
10 | entryPoints: ["./build/index.js"],
11 | outfile: "./build/index.js",
12 | bundle: true,
13 | minify: mode === "production",
14 | sourcemap: mode !== "production",
15 | allowOverwrite: true,
16 | define: {
17 | "process.env.NODE_ENV": JSON.stringify(mode), __dirname: JSON.stringify(__dirname)
18 | },
19 | plugins: [NodeModulesPolyfillPlugin()],
20 | inject: ["./process-env-shim.js"]
21 | });
22 | }
23 |
24 | build()
25 | .then(() => {
26 | console.log("Build complete");
27 | })
28 | .catch((err) => {
29 | console.error(err);
30 | process.exit(1);
31 | });
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Marco Antônio Mafessolli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import type { LoaderFunction, MetaFunction } from '@remix-run/cloudflare'
2 | import { json } from '@remix-run/cloudflare'
3 | import { useLoaderData, Link } from '@remix-run/react'
4 |
5 | import { db } from '~/db.server'
6 | import type { User } from '@prisma/client'
7 |
8 | export let meta: MetaFunction = () => {
9 | return {
10 | title: 'Remix + Prisma + Cloudflare Workers',
11 | description: 'Remix + Prisma + Cloudflare Workers example project',
12 | }
13 | }
14 |
15 | export let loader: LoaderFunction = async () => {
16 | const users = await db.user.findMany({
17 | take: 5,
18 | select: { id: true, name: true },
19 | })
20 |
21 | return json(users)
22 | }
23 |
24 | export default function Index() {
25 | const users = useLoaderData()
26 |
27 | return (
28 |
29 |
Users
30 |
31 |
32 | {users.map((user) => (
33 | -
34 |
35 | {user.name}
36 |
37 |
38 | ))}
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/app/routes/users/$id.tsx:
--------------------------------------------------------------------------------
1 | import type { LoaderFunction, MetaFunction, HeadersFunction } from '@remix-run/node'
2 | import { json } from '@remix-run/cloudflare'
3 | import { useLoaderData, Link } from '@remix-run/react'
4 |
5 |
6 | import { db } from '~/db.server'
7 | import type { User } from '@prisma/client'
8 |
9 | export let meta: MetaFunction = ({ data }: { data: User | undefined }) => {
10 | if (!data) {
11 | return {
12 | title: 'User not found',
13 | description: 'User not found',
14 | }
15 | }
16 |
17 | return {
18 | title: `User ${data.name}`,
19 | description: `User ${data.name} details`,
20 | }
21 | }
22 |
23 | export let loader: LoaderFunction = async ({ params }) => {
24 | try {
25 | const user = await db.user.findUnique({
26 | where: {
27 | id: params.id,
28 | },
29 | })
30 |
31 | return json(user)
32 | } catch (error) {
33 | throw new Response('Not Found', {
34 | status: 404,
35 | })
36 | }
37 | }
38 |
39 | export let headers: HeadersFunction = () => {
40 | return {
41 | 'Cache-Control': 'max-age=60',
42 | }
43 | }
44 |
45 | export default function Index() {
46 | const user = useLoaderData()
47 |
48 | return (
49 |
50 |
{user.name}
51 |
{user.email}
52 |
Back to users
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | x# Remix + Cloudflare Workers + Prisma!
2 |
3 | - [Remix Docs](https://remix.run/docs)
4 |
5 | ## Live Demo
6 |
7 | - [Demo](https://remix-cloudflare-workers.marcomafessolli.workers.dev)
8 |
9 | ## Setup
10 |
11 | Before starting, make sure you have the following:
12 |
13 | * `node >= 16`
14 | * A Prisma Data Proxy account
15 | * Followed [Deploying to Cloudflare Workers](https://www.prisma.io/docs/guides/deployment/deployment-guides/deploying-to-cloudflare-workers) documentation and did steps 2, 6 and 7
16 | * A `.env` file with `DATABASE_URL` that points to your prisma data proxy account
17 |
18 | For more information, visit [this](https://www.prisma.io/docs/concepts/data-platform/data-proxy#edge-runtimes)
19 |
20 | After that, run:
21 |
22 | * `npm install`
23 |
24 | Edge functions cannot access files in the file system which prevents Prisma to load values from `.env`, so it is necessary to set up a wrangler secret by using wrangler's CLI or the dashboard.
25 |
26 | ```sh
27 | $ wrangler secret put DATABASE_URL
28 | ```
29 |
30 | And finally, generate a new Prisma Client by running:
31 |
32 | * `npm run generate-client`
33 |
34 | Check [Prisma and Cloudflare](https://www.prisma.io/docs/guides/deployment/deployment-guides/deploying-to-cloudflare-workers) for more information.
35 |
36 | ## Development
37 |
38 | ```sh
39 | $ npm run dev
40 | ```
41 |
42 | And then check `http://127.0.0.1:8787`. You're ready 💇♂️
43 |
44 | ## Deployment
45 |
46 | ```sh
47 | npm run deploy
48 | ```
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "remix-app-template",
4 | "main": "./build/index.js",
5 | "scripts": {
6 | "clean": "rimraf .cache .mf build dist",
7 | "dev": "npm run build && cross-env NODE_ENV=development concurrently \"npm:dev:*\"",
8 | "dev:remix": "remix watch",
9 | "dev:worker": "npm run build && cross-env NODE_ENV=development miniflare --build-command \"node ./scripts/build.js\" -w -d",
10 | "build": "npm run clean && remix build",
11 | "deploy": "npm run build && wrangler publish",
12 | "format": "prettier --write \"**/*.{js,ts,jsx,tsx,json,md,yml}\"",
13 | "generate-client": "prisma generate --data-proxy",
14 | "migrate": "DATABASE_URL=\"$MIGRATE_DATABASE_URL\" prisma migrate deploy"
15 | },
16 | "dependencies": {
17 | "@prisma/client": "^3.15.2",
18 | "@remix-run/cloudflare": "^1.6.1",
19 | "@remix-run/cloudflare-workers": "^1.6.1",
20 | "@remix-run/react": "^1.6.1",
21 | "react": "^18.2.0",
22 | "react-dom": "^18.2.0"
23 | },
24 | "devDependencies": {
25 | "@cloudflare/workers-types": "^3.13.0",
26 | "@cloudflare/wrangler": "^1.19.12",
27 | "@esbuild-plugins/node-modules-polyfill": "^0.1.4",
28 | "@remix-run/dev": "^1.5.1",
29 | "@types/react": "^18.0.12",
30 | "@types/react-dom": "^18.0.5",
31 | "concurrently": "^7.2.1",
32 | "cross-env": "^7.0.3",
33 | "esbuild": "^0.14.43",
34 | "esbuild-plugin-alias": "^0.2.1",
35 | "miniflare": "^2.5.0",
36 | "prettier": "^2.6.2",
37 | "prisma": "^3.15.2",
38 | "rimraf": "^3.0.2",
39 | "typescript": "^4.7.3"
40 | },
41 | "engines": {
42 | "node": ">=14"
43 | },
44 | "sideEffects": false
45 | }
46 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: '🐛 Bug report'
2 | description: Create a report to help us improve
3 | body:
4 | - type: markdown
5 | attributes:
6 | value: |
7 | Thank you for reporting an issue :pray:.
8 |
9 | This issue tracker is for reporting bugs found in `remix-prisma-cloudflare-workers` (https://github.com/marcomafessolli/remix-prisma-cloudflare-workers).
10 | If you have a question about how to achieve something and are struggling, please post a question
11 | inside of `remix-prisma-cloudflare-workers` Discussions tab: https://github.com/marcomafessolli/remix-prisma-cloudflare-workers/discussions
12 |
13 | Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:
14 | - `remix-prisma-cloudflare-workers` Issues tab: https://github.com/marcomafessolli/remix-prisma-cloudflare-workers/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc
15 | - `remix-prisma-cloudflare-workers` closed issues tab: https://github.com/marcomafessolli/remix-prisma-cloudflare-workers/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed
16 | - `remix-prisma-cloudflare-workers` Discussions tab: https://github.com/marcomafessolli/remix-prisma-cloudflare-workers/discussions
17 |
18 | The more information you fill in, the better the community can help you.
19 | - type: textarea
20 | id: description
21 | attributes:
22 | label: Describe the bug
23 | description: Provide a clear and concise description of the challenge you are running into.
24 | validations:
25 | required: true
26 | - type: input
27 | id: link
28 | attributes:
29 | label: Your Example Website or App
30 | description: |
31 | Which website or app were you using when the bug happened?
32 | Note:
33 | - Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the `remix-prisma-cloudflare-workers` npm package.
34 | - To create a shareable code example you can use Stackblitz (https://stackblitz.com/). Please no localhost URLs.
35 | - Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve.
36 | placeholder: |
37 | e.g. https://stackblitz.com/edit/...... OR Github Repo
38 | validations:
39 | required: true
40 | - type: textarea
41 | id: steps
42 | attributes:
43 | label: Steps to Reproduce the Bug or Issue
44 | description: Describe the steps we have to take to reproduce the behavior.
45 | placeholder: |
46 | 1. Go to '...'
47 | 2. Click on '....'
48 | 3. Scroll down to '....'
49 | 4. See error
50 | validations:
51 | required: true
52 | - type: textarea
53 | id: expected
54 | attributes:
55 | label: Expected behavior
56 | description: Provide a clear and concise description of what you expected to happen.
57 | placeholder: |
58 | As a user, I expected ___ behavior but i am seeing ___
59 | validations:
60 | required: true
61 | - type: textarea
62 | id: screenshots_or_videos
63 | attributes:
64 | label: Screenshots or Videos
65 | description: |
66 | If applicable, add screenshots or a video to help explain your problem.
67 | For more information on the supported file image/file types and the file size limits, please refer
68 | to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
69 | placeholder: |
70 | You can drag your video or image files inside of this editor ↓
71 | - type: textarea
72 | id: platform
73 | attributes:
74 | label: Platform
75 | value: |
76 | - OS: [e.g. macOS, Windows, Linux]
77 | - Browser: [e.g. Chrome, Safari, Firefox]
78 | - Version: [e.g. 91.1]
79 | validations:
80 | required: true
81 | - type: textarea
82 | id: additional
83 | attributes:
84 | label: Additional context
85 | description: Add any other context about the problem here.
86 |
--------------------------------------------------------------------------------