diff --git a/nextjs-app-stack/.gitignore b/nextjs-app-stack/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/nextjs-app-stack/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/nextjs-app-stack/README.md b/nextjs-app-stack/README.md new file mode 100644 index 0000000..053bdc5 --- /dev/null +++ b/nextjs-app-stack/README.md @@ -0,0 +1,23 @@ +# Hono stack with Next.js App router + +- Next.js +- Hono +- Zod +- Zod Validator Middleware +- `hc` +- React query + +## Usage + +``` +yarn +yarn dev +``` + +## Author + +Sermeus Steven + +## License + +MIT diff --git a/nextjs-app-stack/app/api/[[...route]]/route.ts b/nextjs-app-stack/app/api/[[...route]]/route.ts new file mode 100644 index 0000000..9bb1b62 --- /dev/null +++ b/nextjs-app-stack/app/api/[[...route]]/route.ts @@ -0,0 +1,24 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { handle } from "hono/vercel"; +import { z } from "zod"; +export const dynamic = "force-dynamic"; + +const app = new Hono().basePath("/api"); + +const schema = z.object({ + name: z.string(), +}); + +const route = app.post("/echo", zValidator("json", schema), async (c) => { + const { name } = c.req.valid("json"); + return c.json({ name }); +}); + +export type AppType = typeof route; + +export const GET = handle(app); +export const POST = handle(app); +export const PUT = handle(app); +export const DELETE = handle(app); +export const PATCH = handle(app); diff --git a/nextjs-app-stack/app/layout.tsx b/nextjs-app-stack/app/layout.tsx new file mode 100644 index 0000000..df8e790 --- /dev/null +++ b/nextjs-app-stack/app/layout.tsx @@ -0,0 +1,21 @@ +import ReactQueryProvider from "@/components/providers"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Hono example", + description: "Next.js app router with Hono zod validation", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/nextjs-app-stack/app/page.tsx b/nextjs-app-stack/app/page.tsx new file mode 100644 index 0000000..7c52f07 --- /dev/null +++ b/nextjs-app-stack/app/page.tsx @@ -0,0 +1,40 @@ +"use client"; +import { $api } from "@/lib/api"; +import { useMutation } from "@tanstack/react-query"; +import { InferRequestType, InferResponseType } from "hono"; +import { useRef } from "react"; + +export default function Home() { + const $post = $api.echo.$post; + const nameRef = useRef(null); + const mutation = useMutation< + InferResponseType, + Error, + InferRequestType["json"] + >({ + mutationFn: async (data) => { + const res = await $post({ + json: data, + }); + return await res.json(); + }, + mutationKey: ["echo"], + }); + + return ( + <> + + + {mutation.isSuccess &&
Hello {mutation.data.name}
} + + ); +} diff --git a/nextjs-app-stack/components/providers.tsx b/nextjs-app-stack/components/providers.tsx new file mode 100644 index 0000000..2aea89e --- /dev/null +++ b/nextjs-app-stack/components/providers.tsx @@ -0,0 +1,49 @@ +"use client"; + +import React from "react"; + +import SuperJSON from "superjson"; + +import { + defaultShouldDehydrateQuery, + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +interface ReactQueryProviderProps { + children: React.ReactNode; +} + +export default function ReactQueryProvider({ + children, +}: ReactQueryProviderProps) { + const [queryClient] = React.useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 30 * 1000, + }, + + dehydrate: { + serializeData: SuperJSON.serialize, + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === "pending", + }, + hydrate: { + deserializeData: SuperJSON.deserialize, + }, + }, + }) + ); + return ( + + {children} + + + ); +} diff --git a/nextjs-app-stack/lib/api.ts b/nextjs-app-stack/lib/api.ts new file mode 100644 index 0000000..d0aab9d --- /dev/null +++ b/nextjs-app-stack/lib/api.ts @@ -0,0 +1,4 @@ +import { AppType } from "@/app/api/[[...route]]/route"; +import { hc } from "hono/client"; + +export const $api = hc("/").api; diff --git a/nextjs-app-stack/next.config.mjs b/nextjs-app-stack/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/nextjs-app-stack/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/nextjs-app-stack/package.json b/nextjs-app-stack/package.json new file mode 100644 index 0000000..67abc54 --- /dev/null +++ b/nextjs-app-stack/package.json @@ -0,0 +1,28 @@ +{ + "name": "nextjs-app-stack", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@hono/zod-validator": "^0.3.0", + "@tanstack/react-query": "^5.59.0", + "@tanstack/react-query-devtools": "^5.59.0", + "hono": "^4.6.3", + "next": "14.2.14", + "react": "^18", + "react-dom": "^18", + "superjson": "^2.2.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "typescript": "^5" + } +} diff --git a/nextjs-app-stack/tsconfig.json b/nextjs-app-stack/tsconfig.json new file mode 100644 index 0000000..e7ff90f --- /dev/null +++ b/nextjs-app-stack/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/nextjs-page-stack/.gitignore b/nextjs-page-stack/.gitignore new file mode 100644 index 0000000..f74c781 --- /dev/null +++ b/nextjs-page-stack/.gitignore @@ -0,0 +1,2 @@ +.next +node_modules diff --git a/nextjs-page-stack/README.md b/nextjs-page-stack/README.md new file mode 100644 index 0000000..f500287 --- /dev/null +++ b/nextjs-page-stack/README.md @@ -0,0 +1,23 @@ +# Hono Stack with Next.js + +* Next.js +* Hono +* Zod +* Zod Validator Middleware +* `hc` +* SWR + +## Usage + +``` +yarn +yarn dev +``` + +## Author + +Yusuke Wada + +## License + +MIT diff --git a/nextjs-page-stack/next-env.d.ts b/nextjs-page-stack/next-env.d.ts new file mode 100644 index 0000000..a4a7b3f --- /dev/null +++ b/nextjs-page-stack/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/nextjs-page-stack/next.config.js b/nextjs-page-stack/next.config.js new file mode 100644 index 0000000..a843cbe --- /dev/null +++ b/nextjs-page-stack/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +module.exports = nextConfig diff --git a/nextjs-page-stack/package.json b/nextjs-page-stack/package.json new file mode 100644 index 0000000..660f8e9 --- /dev/null +++ b/nextjs-page-stack/package.json @@ -0,0 +1,23 @@ +{ + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@hono/zod-validator": "^0.2.1", + "hono": "^4.2.4", + "next": "^14.0.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "swr": "^2.2.4", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/node": "20.14.4", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "typescript": "^5.4.5" + } +} diff --git a/nextjs-page-stack/pages/_app.tsx b/nextjs-page-stack/pages/_app.tsx new file mode 100644 index 0000000..93928c3 --- /dev/null +++ b/nextjs-page-stack/pages/_app.tsx @@ -0,0 +1,5 @@ +import type { AppProps } from 'next/app' + +export default function App({ Component, pageProps }: AppProps) { + return +} diff --git a/nextjs-page-stack/pages/_document.tsx b/nextjs-page-stack/pages/_document.tsx new file mode 100644 index 0000000..ee65e53 --- /dev/null +++ b/nextjs-page-stack/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + + + +
+ + + + ) +} diff --git a/nextjs-page-stack/pages/api/[...route].ts b/nextjs-page-stack/pages/api/[...route].ts new file mode 100644 index 0000000..cf34880 --- /dev/null +++ b/nextjs-page-stack/pages/api/[...route].ts @@ -0,0 +1,25 @@ +import { zValidator } from '@hono/zod-validator' +import { Hono } from 'hono' +import { handle } from 'hono/vercel' +import { z } from 'zod' + +export const config = { + runtime: 'edge' +} + +const schema = z.object({ + name: z.string() +}) + +const app = new Hono().basePath('/api') + +const route = app.post('/hello', zValidator('form', schema), (c) => { + const data = c.req.valid('form') + return c.json({ + message: `Hello ${data.name}!` + }) +}) + +export type AppType = typeof route + +export default handle(app) diff --git a/nextjs-page-stack/pages/index.tsx b/nextjs-page-stack/pages/index.tsx new file mode 100644 index 0000000..ca9a7f8 --- /dev/null +++ b/nextjs-page-stack/pages/index.tsx @@ -0,0 +1,30 @@ +import { hc } from 'hono/client' +import { useState } from 'react' +import useSWRMutation from 'swr/mutation' +import { AppType } from './api/[...route]' + +const client = hc('/') + +const postHello = async (_: string, { arg }: { arg: string }) => { + const res = await client.api.hello.$post({ + form: { + name: arg + } + }) + return await res.json() +} + +export default function Home() { + const { trigger, isMutating, data } = useSWRMutation('post-hello', postHello) + const [name, setName] = useState('') + + return ( +
+ setName(e.target.value)} placeholder="Name" /> + +

{data?.message}

+
+ ) +} diff --git a/nextjs-page-stack/tsconfig.json b/nextjs-page-stack/tsconfig.json new file mode 100644 index 0000000..a0d639b --- /dev/null +++ b/nextjs-page-stack/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index f38364f..0d49fd3 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "serve-static", "bun", "pages-stack", - "nextjs-stack", + "nextjs-page-stack", + "nextjs-app-stack", "hono-vite-jsx" ], "license": "MIT",