Remix API Integration with Nile Database

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:

npx create-react-router@latest --template remix-run/react-router-templates/node-postgres

Follow the prompts and install your app. After creating the project, navigate into the newly created project directory:

cd <your-project-name>
npm install @niledatabase/server @niledatabase/react
2

Obtain Database Credentials

  1. If you haven’t signed up for Nile yet, sign up here and follow the steps to create a database.
  2. Navigate to Database Settings in your database’s UI at console.thenile.dev.
  3. Go to Connection settings.
  4. Select the CLI icon, and click Generate credentials
  5. Copy the required credentials and store them in an .env file so they can be used in the application to connect to the Nile auth service.
  6. While you are there, click on the PostgreSQL icon and also copy your database url for drizzle to use
    NILEDB_USER=niledb_user
    NILEDB_PASSWORD=niledb_password
    NILEDB_API_URL=https://us-west-2.api.thenile.dev/v2/databases/<database_id>
    NILEDB_POSTGRES_URL=postgres://us-west-2.db.thenile.dev:5432/<database_name>
    DATABASE_URL=postgres://niledb_user:niledb_password0@us-west-2.db.thenile.dev:5432/<database_name>
    
3

Update create-react-router output

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
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",
      };
    },
  })
);
EOF

cat > 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;
}
EOF

cat > 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")
);
EOF

cat > 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

4

Add Nile to the server

Now we need to add the nile instance and route handlers to allow our server to respond to authentication, user, and tenant requests.

Bash
cat > app/nile.ts << 'EOF'
import { Nile } from "@niledatabase/server";

export const nile = await Nile();
export const { handlers } = nile.api;
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

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.
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
6

Running the Project

To run your project, execute the following:

npm run db:migrate
npm run build
npm run dev

This will start the development server at http://localhost:3000, and you can test your API endpoints and authentication components.

Summary

Now you can interact with your Nile Database through Remix API routes and manage authentication in your app!

Creating a loader

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.


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>
   );
}

Was this page helpful?