├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── app ├── db.server.ts ├── entry.client.tsx ├── entry.server.tsx ├── root.tsx └── routes │ ├── index.tsx │ └── users │ └── $id.tsx ├── package-lock.json ├── package.json ├── prisma └── schema.prisma ├── process-env-shim.js ├── public └── favicon.ico ├── remix.config.js ├── remix.env.d.ts ├── scripts └── build.js ├── server.ts ├── tsconfig.json └── wrangler.toml /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | 7 | .env 8 | .mf 9 | .idea 10 | .vscode 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | .mf 4 | .cache 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from 'react-dom' 2 | import { RemixBrowser } from '@remix-run/react' 3 | 4 | hydrate(, document) 5 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/8b47b167e53635e3a55b012f5bf11591ce1d2537/public/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------