This guide explains how to integrate Nile Database with Remix and set up routes for handling various HTTP requests (GET, POST, PUT, DELETE). Additionally, you’ll see how to include client-side components for user authentication and interaction using Nile’s React SDK.
1
Create a new Remix project
Run the following command in your terminal to create a new Remix project:
Now we must update the output from the default create-react-router for use with Nile. We want to switch to using node-postgres, and be able to do a top level await for nile configuration.
bash
Copy
Ask AI
cat > server/app.ts << 'EOF'import { createRequestHandler } from "@react-router/express";import { drizzle } from "drizzle-orm/node-postgres";import express from "express";import postgres from "pg";import "react-router";import { DatabaseContext } from "~/database/context";import * as schema from "~/database/schema";declare module "react-router" { interface AppLoadContext { VALUE_FROM_EXPRESS: string; }}export const app = express();if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is required");const client = new postgres.Client(process.env.DATABASE_URL);await client.connect();const db = drizzle(client, { schema });app.use((_, __, next) => DatabaseContext.run(db, next));app.use( createRequestHandler({ build: () => import("virtual:react-router/server-build"), getLoadContext() { return { VALUE_FROM_EXPRESS: "Hello from Express", }; }, }));EOFcat > database/context.ts << 'EOF'import { AsyncLocalStorage } from "node:async_hooks";import type { NodePgDatabase } from "drizzle-orm/node-postgres";import * as schema from "./schema";export const DatabaseContext = new AsyncLocalStorage< NodePgDatabase<typeof schema>>();export function database() { const db = DatabaseContext.getStore(); if (!db) { throw new Error("DatabaseContext not set"); } return db;}EOFcat > drizzle/0000_short_donald_blake.sql << 'EOF'CREATE TABLE IF NOT EXISTS "guestBook" ( "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "name" varchar(255) NOT NULL, "email" varchar(255) NOT NULL, CONSTRAINT "guestBook_email_unique" UNIQUE("email"));EOFcat > vite.config.ts << 'EOF'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(({ isSsrBuild }) => ({ optimizeDeps: { esbuildOptions: { target: "esnext", }, }, build: { target: "esnext", rollupOptions: isSsrBuild ? { input: "./server/app.ts", } : undefined, }, plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],}));EOF
bash
Copy
Ask AI
cat > server/app.ts << 'EOF'import { createRequestHandler } from "@react-router/express";import { drizzle } from "drizzle-orm/node-postgres";import express from "express";import postgres from "pg";import "react-router";import { DatabaseContext } from "~/database/context";import * as schema from "~/database/schema";declare module "react-router" { interface AppLoadContext { VALUE_FROM_EXPRESS: string; }}export const app = express();if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is required");const client = new postgres.Client(process.env.DATABASE_URL);await client.connect();const db = drizzle(client, { schema });app.use((_, __, next) => DatabaseContext.run(db, next));app.use( createRequestHandler({ build: () => import("virtual:react-router/server-build"), getLoadContext() { return { VALUE_FROM_EXPRESS: "Hello from Express", }; }, }));EOFcat > database/context.ts << 'EOF'import { AsyncLocalStorage } from "node:async_hooks";import type { NodePgDatabase } from "drizzle-orm/node-postgres";import * as schema from "./schema";export const DatabaseContext = new AsyncLocalStorage< NodePgDatabase<typeof schema>>();export function database() { const db = DatabaseContext.getStore(); if (!db) { throw new Error("DatabaseContext not set"); } return db;}EOFcat > drizzle/0000_short_donald_blake.sql << 'EOF'CREATE TABLE IF NOT EXISTS "guestBook" ( "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "name" varchar(255) NOT NULL, "email" varchar(255) NOT NULL, CONSTRAINT "guestBook_email_unique" UNIQUE("email"));EOFcat > vite.config.ts << 'EOF'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(({ isSsrBuild }) => ({ optimizeDeps: { esbuildOptions: { target: "esnext", }, }, build: { target: "esnext", rollupOptions: isSsrBuild ? { input: "./server/app.ts", } : undefined, }, plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],}));EOF
Replace /server/app.ts with the following
Copy
Ask AI
import { createRequestHandler } from "@react-router/express";import { drizzle } from "drizzle-orm/node-postgres";import express from "express";import postgres from "pg";import "react-router";import { DatabaseContext } from "~/database/context";import * as schema from "~/database/schema";declare module "react-router" { interface AppLoadContext { VALUE_FROM_EXPRESS: string; }}export const app = express();if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is required");const client = new postgres.Client(process.env.DATABASE_URL);await client.connect();const db = drizzle(client, { schema });app.use((_, __, next) => DatabaseContext.run(db, next));app.use( createRequestHandler({ build: () => import("virtual:react-router/server-build"), getLoadContext() { return { VALUE_FROM_EXPRESS: "Hello from Express", }; }, }));
Replace database/context.ts with the following
Copy
Ask AI
import { AsyncLocalStorage } from "node:async_hooks";import type { NodePgDatabase } from "drizzle-orm/node-postgres";import * as schema from "./schema";export const DatabaseContext = new AsyncLocalStorage< NodePgDatabase<typeof schema>>();export function database() { const db = DatabaseContext.getStore(); if (!db) { throw new Error("DatabaseContext not set"); } return db;}
Update the placeholder table drizzle/0000_short_donald_blake.sql to generate correctly
Copy
Ask AI
CREATE TABLE IF NOT EXISTS "guestBook" ( "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "name" varchar(255) NOT NULL, "email" varchar(255) NOT NULL, CONSTRAINT "guestBook_email_unique" UNIQUE("email"));
Modify vite.config.ts to allow for top level awaits
In some cases, you may want to create specific action and loader around the API, to do that, use the server side functions in the sdk in your loader. This code loads and updates a user’s profile.
Copy
Ask AI
import type { Route } from "./+types/profile";import { nile } from "~/nile";export const loader: LoaderFunction = async ({ request }) => { try { const user = await nile.api.users.me(); if (user) { // If the user is authenticated, we can return their info or pass it to the UI return json({ user }); } else { // If the user is not authenticated, redirect to the index page return redirect("/"); } } catch (error) { return json({ message: error.message }, { status: 500 }); }};export default function Profile({ loaderData }: : Route.ComponentProps) { const { user, message } = loaderData; return ( <div className="container mx-auto flex flex-col items-center pt-20 gap-20 relative"> <div>{message ? <>{message}</> : null}</div> <SignOutButton callbackUrl="/" className="absolute right-0 top-20" /> <UserInfo user={user} className="w-[400px]" /> </div> );}