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.
CLI
Manual
bash
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
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
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
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
Now we need to add the nile instance and route handlers to allow our server to respond to authentication, user, and tenant requests.
CLI
Manual
Bash
cat > app/nile.ts << 'EOF'import { Nile } from "@niledatabase/server";export const nile = Nile();export const { handlers } = nile;EOF;cat > app/routes/nile-api.ts << 'EOF'import type { Route } from "./+types/home";import { handlers } from "~/nile";const { GET, POST, PUT, DELETE } = handlers;export const loader = async ({ request }: Route.LoaderArgs) => { switch (request.method.toUpperCase()) { case "GET": return GET(request); case "POST": return POST(request); case "PUT": return PUT(request); case "DELETE": return DELETE(request); default: return new Response("Method Not Allowed", { status: 405 }); }};export const action = async ({ request }: Route.ActionArgs) => { switch (request.method.toUpperCase()) { case "POST": return POST(request); case "PUT": return PUT(request); case "DELETE": return DELETE(request); default: return new Response("Method Not Allowed", { status: 405 }); }};EOF;cat > app/routes.ts << 'EOF'import { type RouteConfig, index, route } from "@react-router/dev/routes";export default [ index("routes/home.tsx"), route("api/*", "routes/nile-api.ts"),] satisfies RouteConfig;EOF
Create a file to house the main Nile instance. You can use this file to access nile from a central location any where in the app.app/nile.ts
import { Nile } from "@niledatabase/server";export const nile = Nile();export const { handlers } = nile;
Create the API route file at app/routes/nile-api.ts. This file will handle different HTTP methods (GET, POST, PUT, DELETE) using the Nile SDK.
import type { Route } from './+types/home';import { handlers } from '~/nile';const { GET, POST, PUT, DELETE } = handlers;export const loader = async ({ request }: Route.LoaderArgs) => { switch (request.method.toUpperCase()) { case 'GET': return GET(request); case 'POST': return POST(request); case 'PUT': return PUT(request); case 'DELETE': return DELETE(request); default: return new Response('Method Not Allowed', { status: 405 }); }};export const action = async ({ request }: Route.ActionArgs) => { switch (request.method.toUpperCase()) { case 'POST': return POST(request); case 'PUT': return PUT(request); case 'DELETE': return DELETE(request); default: return new Response('Method Not Allowed', { status: 405 }); }};
This code handles different HTTP methods (GET, POST, PUT, DELETE) for the /api/* route and delegates the logic to Nile Database.Update your routes to respond to the apiapp/routes.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes';export default [ index('routes/home.tsx'), route('api/*', 'routes/auth-api.ts'),] satisfies RouteConfig;
5
Add Client-Side Code for Authentication
You can use the components from @niledatabase/react to handle authentication. Replace the boilerplate of the main _index.tsx file with the following:This component will render:
User info and the guest book if the user is signed in.
Sign-up form if the user is not signed in.
CLI
Manual
Bash
cat > app/routes/home.tsx << 'EOF'import { SignedIn, SignedOut, SignOutButton, SignUpForm, UserInfo,} from "@niledatabase/react";import '@niledatabase/react/styles.css';import { database } from '~/database/context';import * as schema from '~/database/schema';import type { Route } from "./+types/home";import { Welcome } from "../welcome/welcome";export function meta({}: Route.MetaArgs) { return [ { title: "New React Router App" }, { name: "description", content: "Welcome to React Router!" }, ];}export async function action({ request }: Route.ActionArgs) { const formData = await request.formData(); let name = formData.get("name"); let email = formData.get("email"); if (typeof name !== "string" || typeof email !== "string") { return { guestBookError: "Name and email are required" }; }name = name.trim();email = email.trim();if (!name || !email) {return { guestBookError: "Name and email are required" };}const db = database();try {await db.insert(schema.guestBook).values({ name, email });} catch (error) {return { guestBookError: "Error adding to guest book" };}}export async function loader({ context }: Route.LoaderArgs) { const db = database();const guestBook = await db.query.guestBook.findMany({columns: {id: true,name: true,},});return {guestBook,message: context.VALUE_FROM_EXPRESS,};}export default function Home({ actionData, loaderData }: Route.ComponentProps) { return ( <div className="w-screen h-screen flex items-center justify-center flex-col"> <SignedIn className="flex flex-col gap-4"> <UserInfo /> <SignOutButton /> <Welcome guestBook={loaderData.guestBook} guestBookError={actionData?.guestBookError} message={loaderData.message} /> </SignedIn> <SignedOut> <SignUpForm /> </SignedOut> </div> );}EOF
Update the render of home.tsx to use the following components:app/routes/home.tsx
import { SignedIn, SignedOut, SignUpForm, UserInfo } from "@niledatabase/react";import "@niledatabase/react/styles.css";{/**rest of the actions/meta/loader*/}export default function Home({ actionData, loaderData }: Route.ComponentProps) { return ( <SignedIn> <Welcome guestBook={loaderData.guestBook} guestBookError={actionData?.guestBookError} message={loaderData.message} /> </SignedIn> <SignedOut> <SignUpForm> </SignedOut> );}
6
Running the Project
To run your project, execute the following:
npm run db:migratenpm run buildnpm run dev
This will start the development server at http://localhost:3000, and you can test your API endpoints and authentication components.