Skip to content

Commit

Permalink
Merge pull request #62 from neondatabase/andrelandgraf/with-react-router
Browse files Browse the repository at this point in the history
With react-router example
  • Loading branch information
rishi-raj-jain authored Jan 31, 2025
2 parents bd04444 + ee02a5b commit cc85097
Show file tree
Hide file tree
Showing 15 changed files with 383 additions and 0 deletions.
4 changes: 4 additions & 0 deletions with-react-router/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.react-router
build
node_modules
README.md
1 change: 1 addition & 0 deletions with-react-router/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DATABASE_URL="postgresql://neondb_owner:...@ep-...us-east-1.aws.neon.tech/neondb?sslmode=require"
6 changes: 6 additions & 0 deletions with-react-router/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
/node_modules/

# React Router
/.react-router/
/build/
22 changes: 22 additions & 0 deletions with-react-router/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM node:20-alpine AS development-dependencies-env
COPY . /app
WORKDIR /app
RUN npm ci

FROM node:20-alpine AS production-dependencies-env
COPY ./package.json package-lock.json /app/
WORKDIR /app
RUN npm ci --omit=dev

FROM node:20-alpine AS build-env
COPY . /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN npm run build

FROM node:20-alpine
COPY ./package.json package-lock.json /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["npm", "run", "start"]
114 changes: 114 additions & 0 deletions with-react-router/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<img width="250px" src="https://neon.tech/brand/neon-logo-dark-color.svg" />

# Getting started with Neon and React Router

## Clone the repository

```bash
npx degit neondatabase/examples/with-react-router ./with-react-router
```

## Create a .env file

Run the command below to copy the `.env.example` file:

```bash
cp .env.example .env
```

## Store your Neon credentials

Store your Neon credentials in the `.env` file. You can obtain the connection string for your database from the Connection Details widget on the Neon Dashboard.

```
DATABASE_URL="postgresql://neondb_owner:...@ep-...us-east-1.aws.neon.tech/neondb?sslmode=require"
```

- `user` is the database user.
- `password` is the database user’s password.
- `endpoint_hostname` is the host with neon.tech as the [TLD](https://www.cloudflare.com/en-gb/learning/dns/top-level-domain/).
- `dbname` is the name of the database. “neondb” is the default database created with each Neon project.
- `?sslmode=require` an optional query parameter that enforces the [SSL](https://www.cloudflare.com/en-gb/learning/ssl/what-is-ssl/) mode while connecting to the Postgres instance for better security.

**Important**: To ensure the security of your data, never expose your Neon credentials to the browser.

## Install dependencies

Run the command below to install project dependencies:

```bash
npm install
```

## Development

Run the application using the following command:

```bash
npm run dev
```

Your application will be available at `http://localhost:5173`.

## Building for Production

Create a production build:

```bash
npm run build
```

## Deployment

React Router can be deployed to many different deployment targets including Vercel, Netlify, Cloudflare Workers, and more. Check out [React Router's official templates on GitHub](https://github.com/remix-run/react-router-templates) for more information. This template is based on React Router's default template running a standard Node server. It also ships with example Docker files for dockerized deployments.

### Docker Deployment

This template includes three Dockerfiles optimized for different package managers:

- `Dockerfile` - for npm
- `Dockerfile.pnpm` - for pnpm
- `Dockerfile.bun` - for bun

To build and run using Docker:

```bash
# For npm
docker build -t my-app .

# For pnpm
docker build -f Dockerfile.pnpm -t my-app .

# For bun
docker build -f Dockerfile.bun -t my-app .

# Run the container
docker run -p 3000:3000 my-app
```

The containerized application can be deployed to any platform that supports Docker, including:

- AWS ECS
- Google Cloud Run
- Azure Container Apps
- Digital Ocean App Platform
- Fly.io
- Railway

### DIY Deployment

If you're familiar with deploying Node applications, the built-in app server is production-ready.

Make sure to deploy the output of `npm run build`

```
├── package.json
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
├── build/
│ ├── client/ # Static assets
│ └── server/ # Server-side code
```

## Styling

This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
11 changes: 11 additions & 0 deletions with-react-router/app/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import "tailwindcss";

@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}

html,
body {
@apply bg-gray-950;
}
75 changes: 75 additions & 0 deletions with-react-router/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";

import type { Route } from "./+types/root";
import "./app.css";

export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
];

export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

export default function App() {
return <Outlet />;
}

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
let details = "An unexpected error occurred.";
let stack: string | undefined;

if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error";
details =
error.status === 404
? "The requested page could not be found."
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}

return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<code>{stack}</code>
</pre>
)}
</main>
);
}
3 changes: 3 additions & 0 deletions with-react-router/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { type RouteConfig, index } from "@react-router/dev/routes";

export default [index("routes/home.tsx")] satisfies RouteConfig;
21 changes: 21 additions & 0 deletions with-react-router/app/routes/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { neon } from "@neondatabase/serverless";
import type { Route } from "./+types/home";
import { Welcome } from "../welcome/welcome";

export function meta({}: Route.MetaArgs) {
return [
{ title: "Neon with React Router" },
{ name: "description", content: "Welcome to React Router + Neon!" },
];
}

export async function loader({}: Route.ClientLoaderArgs) {
const sql = neon(`${process.env.DATABASE_URL}`);
const response = await sql`SELECT version()`;
const { version } = response[0];
return { version };
}

export default function Home({ loaderData }: Route.ComponentProps) {
return <Welcome databaseVersion={loaderData.version} />;
}
49 changes: 49 additions & 0 deletions with-react-router/app/welcome/welcome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export function Welcome({ databaseVersion }: { databaseVersion: string }) {
return (
<main className="flex items-center justify-center pt-16 pb-4">
<div className="flex-1 flex flex-col items-center gap-16 min-h-0">
<header className="flex flex-col items-center gap-9">
<div className="w-[500px] max-w-[100vw] p-4">
<img
src="https://neon.tech/brand/neon-logo-dark-color.svg"
alt="React Router"
className="w-full"
/>
</div>
<p className="text-white font-semibold text-sm">
Database version: {databaseVersion}
</p>
</header>
<div className="max-w-[300px] w-full space-y-6 px-4">
<nav className="rounded-3xl border p-4 border-gray-700">
<ul className="flex flex-col gap-4">
{resources.map(({ href, text }) => (
<li key={href}>
<a
className="p-4 leading-normal hover:underline text-blue-500"
href={href}
target="_blank"
rel="noreferrer"
>
{text}
</a>
</li>
))}
</ul>
</nav>
</div>
</div>
</main>
);
}

const resources = [
{
href: "https://reactrouter.com/docs",
text: "React Router Docs",
},
{
href: "https://neon.tech/docs/introduction",
text: "Neon Docs",
},
];
31 changes: 31 additions & 0 deletions with-react-router/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"type": "module",
"name": "with-react-router",
"scripts": {
"dev": "react-router dev",
"build": "react-router build",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
"@neondatabase/serverless": "^0.10.4",
"@react-router/node": "^7.1.4",
"@react-router/serve": "^7.1.4",
"isbot": "^5.1.17",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.1.4"
},
"devDependencies": {
"@react-router/dev": "^7.1.4",
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^20",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
"react-router-devtools": "^1.1.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.2",
"vite": "^5.4.11",
"vite-tsconfig-paths": "^5.1.4"
}
}
Binary file added with-react-router/public/favicon.ico
Binary file not shown.
7 changes: 7 additions & 0 deletions with-react-router/react-router.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Config } from "@react-router/dev/config";

export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: true,
} satisfies Config;
27 changes: 27 additions & 0 deletions with-react-router/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"include": [
"**/*",
"**/.server/**/*",
"**/.client/**/*",
".react-router/types/**/*"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"],
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true
}
}
12 changes: 12 additions & 0 deletions with-react-router/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
build: {
cssMinify: true,
ssr: false,
},
});

0 comments on commit cc85097

Please sign in to comment.