├── .eslintrc.js ├── pnpm-workspace.yaml ├── packages └── api │ ├── src │ ├── routes │ │ ├── index.ts │ │ └── v1.0 │ │ │ ├── index.ts │ │ │ └── accounts │ │ │ ├── index.ts │ │ │ └── users.ts │ ├── index.ts │ └── client.ts │ ├── wrangler.toml │ ├── README.md │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── package.json ├── apps └── app │ ├── remix.env.d.ts │ ├── public │ └── favicon.ico │ ├── .eslintrc.js │ ├── remix.config.js │ ├── tsconfig.json │ ├── app │ ├── root.tsx │ └── routes │ │ └── _index.tsx │ ├── README.md │ └── package.json ├── .gitignore ├── turbo.json ├── package.json └── README.md /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | }; 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /packages/api/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | export { default as v1_0 } from "./v1.0"; 2 | -------------------------------------------------------------------------------- /packages/api/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "api" 2 | main = "src/index.ts" 3 | compatibility_date = "2023-01-01" 4 | -------------------------------------------------------------------------------- /apps/app/remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /apps/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergiocarneiro/example-hono-api/HEAD/apps/app/public/favicon.ico -------------------------------------------------------------------------------- /packages/api/README.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | A [Cloudflare Worker](https://workers.cloudflare.com) that exposes a [RPC client](https://hono.dev/guides/rpc). 4 | -------------------------------------------------------------------------------- /apps/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], 4 | }; 5 | -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { v1_0 } from "./routes"; 3 | 4 | const app = new Hono() 5 | .route("/v1.0", v1_0); 6 | 7 | export default app; 8 | -------------------------------------------------------------------------------- /packages/api/src/routes/v1.0/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import accounts from "./accounts"; 3 | 4 | const v1_0 = new Hono() 5 | .route("/accounts", accounts) 6 | 7 | export default v1_0; 8 | -------------------------------------------------------------------------------- /packages/api/src/routes/v1.0/accounts/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import users from "./users"; 3 | 4 | const accounts = new Hono() 5 | .route("/users", users); 6 | 7 | export default accounts; 8 | -------------------------------------------------------------------------------- /packages/api/src/client.ts: -------------------------------------------------------------------------------- 1 | import { hc } from "hono/client"; 2 | import app from "."; 3 | 4 | const host = "http://localhost:8787"; 5 | // const host = "https://api.example.com"; 6 | 7 | export const RPC = hc(host); 8 | -------------------------------------------------------------------------------- /packages/api/src/routes/v1.0/accounts/users.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | 3 | const users = new Hono() 4 | .get("/", (c) => c.jsonT({ message: "GET Users" })) 5 | .post("/", (c) => c.jsonT({ message: "POST Users" })) 6 | 7 | export default users; 8 | -------------------------------------------------------------------------------- /apps/app/remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | module.exports = { 3 | future: { 4 | v2_routeConvention: true, 5 | }, 6 | ignoredRouteFiles: ["**/.*"], 7 | watchPaths: ["../../packages/api"], // automatically reloads when the api package changes ✨ 8 | }; 9 | -------------------------------------------------------------------------------- /packages/api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2015", 5 | "module": "ES2020", 6 | "lib": ["ES2021"], 7 | "outDir": "build", 8 | "declaration": true, 9 | "declarationDir": "build/types", 10 | "sourceMap": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # environment 12 | .env 13 | 14 | # output 15 | .cache 16 | .turbo 17 | out 18 | build 19 | dist 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .pnpm-debug.log* 30 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "ES2022", 5 | "lib": ["ES2021"], 6 | "baseUrl": ".", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "types": ["@cloudflare/workers-types", "node"], 13 | "paths": { 14 | "~/*": ["./src/*"] 15 | } 16 | }, 17 | "include": ["src"] 18 | } 19 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["build/**"], 8 | "outputMode": "new-only" 9 | }, 10 | "dev": { 11 | "dependsOn": ["^build"], 12 | "cache": false, 13 | "persistent": true 14 | }, 15 | "lint": { 16 | "outputs": [] 17 | }, 18 | "test": { 19 | "dependsOn": ["^build"], 20 | "outputs": [], 21 | "cache": false 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hono-cf-api-remix", 3 | "license": "MIT", 4 | "private": true, 5 | "workspaces": [ 6 | "apps/*", 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "build": "turbo run build", 11 | "dev": "turbo run dev", 12 | "lint": "turbo run lint", 13 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 14 | }, 15 | "devDependencies": { 16 | "esbuild": "^0.17.11", 17 | "npm-run-all": "^4.1.5", 18 | "prettier": "^2.8.4", 19 | "turbo": "^1.8.3" 20 | }, 21 | "engines": { 22 | "node": ">=14.0.0" 23 | }, 24 | "packageManager": "pnpm@7.28.0" 25 | } 26 | -------------------------------------------------------------------------------- /apps/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | 19 | // Remix takes care of building everything in `remix build`. 20 | "noEmit": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/app/app/root.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction } from "@remix-run/node"; 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: "example-hono-api", 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 | -------------------------------------------------------------------------------- /apps/app/README.md: -------------------------------------------------------------------------------- 1 | # Remix App 2 | 3 | A [Remix](https://remix.run/) app that consumes the API. 4 | 5 | 6 | ## Perks 7 | 8 | ### Enable end-to-end live reloading 9 | 10 | ```js 11 | // remix.config.js 12 | { 13 | watchPaths: ["../../packages/api"] 14 | } 15 | ``` 16 | 17 | ### Throw failing requests 18 | 19 | And handle them in isolation in the `CatchBoundary` 20 | 21 | ```tsx 22 | export async function loader() { 23 | const res = await RPC.example.$get(); 24 | 25 | if (!res.ok) { 26 | throw res; // 🦄 27 | } 28 | 29 | return res.json(); 30 | } 31 | 32 | export function CatchBoundary() { 33 | const { data, status, statusText } = useCatch(); 34 | 35 | console.error(data); 36 | 37 | return ( 38 |

{`${status} ${statusText}`}

39 | ); 40 | } 41 | 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /apps/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "private": true, 4 | "scripts": { 5 | "build": "remix build", 6 | "dev": "remix dev", 7 | "start": "remix-serve build", 8 | "typecheck": "tsc" 9 | }, 10 | "dependencies": { 11 | "@remix-run/node": "^1.14.1", 12 | "@remix-run/react": "^1.14.1", 13 | "@remix-run/serve": "^1.14.1", 14 | "api": "workspace:^1.0.0", 15 | "isbot": "^3.6.5", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0" 18 | }, 19 | "devDependencies": { 20 | "@remix-run/dev": "^1.14.1", 21 | "@remix-run/eslint-config": "^1.14.1", 22 | "@types/react": "^18.0.25", 23 | "@types/react-dom": "^18.0.8", 24 | "eslint": "^8.27.0", 25 | "typescript": "^4.8.4" 26 | }, 27 | "engines": { 28 | "node": ">=14" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "./src/client.ts", 6 | "types": "./build/types/client.d.ts", 7 | "exports": { 8 | ".": { 9 | "require": "./build/client.cjs", 10 | "import": "./build/client.esm.js" 11 | } 12 | }, 13 | "scripts": { 14 | "build": "run-p build:*", 15 | "build:cjs": "esbuild ./src/client.ts --bundle --format=cjs --platform=node --outfile=build/client.cjs", 16 | "build:esm": "esbuild ./src/client.ts --bundle --format=esm --platform=node --outfile=build/client.esm.js", 17 | "build:types": "tsc -P ./tsconfig.build.json --emitDeclarationOnly", 18 | "dev": "wrangler dev src/index.ts", 19 | "deploy": "wrangler publish src/index.ts" 20 | }, 21 | "dependencies": { 22 | "hono": "^3.2.0" 23 | }, 24 | "devDependencies": { 25 | "@cloudflare/workers-types": "^4.20230307.0", 26 | "@types/node": "^18.15.0", 27 | "wrangler": "^2.12.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/app/app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import { useCatch, useLoaderData } from "@remix-run/react"; 2 | import { RPC } from "api"; 3 | 4 | export async function loader() { 5 | const res = await RPC["v1.0"].accounts.users.$get(); 6 | 7 | if (!res.ok) { 8 | throw res; 9 | } 10 | 11 | return res.json(); 12 | } 13 | 14 | export default function Component() { 15 | const data = useLoaderData(); 16 | 17 | return ( 18 |
19 |

example-hono-api — Remix 💿

20 |
21 |
{JSON.stringify(data, null, 2)}
22 |
23 | ); 24 | } 25 | 26 | export function CatchBoundary() { 27 | const { data, status, statusText } = useCatch(); 28 | 29 | console.error(data); 30 | 31 | return ( 32 |
33 |

34 | {`${status} ${statusText}`} 35 |

36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # example-hono-api 2 | 3 | An example of how to create a publishable and fully type-safe API using [Hono](https://hono.dev). 4 | 5 | A [RPC client](https://hono.dev/guides/rpc) is exposed and can be published as a package to be consumed by other applications. 6 | 7 | ## What's inside? 8 | 9 | ### Projects 10 | 11 | - `api` – a [Cloudflare Worker](https://workers.cloudflare.com) that exposes a [RPC client](https://hono.dev/guides/rpc) 12 | - `app` – a [Remix](https://remix.run/) app that consumes the API 13 | 14 | ### Tech Stack 15 | 16 | - [Hono](https://hono.dev) 17 | - [Cloudflare Workers](https://workers.cloudflare.com) 18 | - [Remix](https://remix.run) 19 | - [Turborepo](https://turbo.build/repo) 20 | - [pnpm](https://pnpm.io) 21 | 22 | ## Setup 23 | 24 |
25 | Environment setup 26 | 27 | ### Install pnpm 28 | 29 | ```sh 30 | corepack prepare pnpm@latest --activate 31 | ``` 32 | [More alternatives](https://pnpm.io/installation) 33 |
34 | 35 | ### Install dependencies 36 | 37 | ```sh 38 | pnpm install -r 39 | ``` 40 | 41 | ## Usage 42 | 43 | Run commands project-wide with `pnpm run `. 44 | 45 | ### Commands 46 | 47 | - `dev` – start the development servers 48 | - `build` – build the production bundles 49 | - `lint` – lint the codebase 50 | - `test` – run the tests 51 | 52 | ### Dev 53 | 54 | #### 1. Start the development servers 55 | 56 | ```sh 57 | pnpm run dev 58 | ``` 59 | 60 | You should see an output containing: 61 | 62 | ```sh 63 | api:dev: ⬣ Listening at http://0.0.0.0:8787 64 | ``` 65 | ```sh 66 | app:dev: Remix App Server started at http://localhost:3000 67 | ``` 68 | 69 | #### 2. Open the app 70 | 71 | Find the `app` URL in the output and open it in your browser. 72 | 73 | #### 3. Making changes 74 | 75 | You can make changes in any of the projects and the development servers will automatically reload ✨ 76 | 77 | ## Perks 78 | 79 | - [Using with Remix](https://github.com/sergiocarneiro/example-hono-api/blob/main/apps/app/README.md) 80 | --------------------------------------------------------------------------------