Skip to main content
The Nile-JS SDK includes generated routes for all its operations. These routes are used by the SDK methods to proxy requests to nile-auth, as well as directly from the React hooks and components in @niledatabase/react.

Generating routes

Route generation depends on the framework you are using.

Next.js example

// app/api/[...nile]/nile.ts
import { Nile } from "@niledatabase/server";

export const nile = Nile();

export const { handlers } = nile;
// app/api/[...nile]/route.ts
import { handlers } from './nile';

export const { POST, GET, DELETE, PUT } = handlers;

Remix example

// app/nile.ts
import { Nile } from "@niledatabase/server";

export const nile = Nile();
export const { handlers } = nile;
//app/routes/nile-api.ts
// This is where the routes are exported
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 });
  }
};
// app/routes.ts
// This routes all the api/* requests to the routes in nile-api.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes';

export default [
  index('routes/home.tsx'),
  route('api/*', 'routes/nile-api.ts'),
] satisfies RouteConfig;

Express example

import "dotenv/config";
import express from "express";
import { Nile } from "@niledatabase/server";
import { NileExpressHandler } from "@niledatabase/server/express";

const startServer = async () => {
try {
const app = express();
const nile = Nile();
// This is where the route handlers are imported
const { paths, handler } = await NileExpressHandler(nile);

        app.use(express.json());
        app.use(express.urlencoded({ extended: true }));

        // This is where the routes are exposed in your express app
        app.get(paths.get, handler);
        app.post(paths.post, handler);
        app.put(paths.put, handler);
        app.delete(paths.delete, handler);

        // Your own routes go here

        const PORT = process.env.PORT || 3040;
        app.listen(PORT, () => {
            console.log(`Server is running on port ${PORT}`);
        });
    } catch (error) {
        console.error("Error starting server:", error);
        process.exit(1);
    }

};

startServer();

Using routes

You typically don’t need to use the routes directly. The SDK methods and react/web components use the routes under the hood to communicate with nile-auth. The generated routes are available via nile.paths. For reference, here are the paths for the default routes:
export const appRoutes = (prefix = '/api'): Routes => ({
  SIGNIN: `${prefix}/auth/signin`,
  PROVIDERS: `${prefix}/auth/providers`,
  SESSION: `${prefix}/auth/session`,
  CSRF: `${prefix}/auth/csrf`,
  CALLBACK: `${prefix}/auth/callback`, // this path has a route per enabled provider (e.g. google, github, etc.)
  SIGNOUT: `${prefix}/auth/signout`,
  ERROR: `${prefix}/auth/error`,
  VERIFY_REQUEST: `${prefix}/auth/verify-request`,
  PASSWORD_RESET: `${prefix}/auth/reset-password`,
  ME: `${prefix}/me`,
  USERS: `${prefix}/users`,
  TENANTS: `${prefix}/tenants`,
  TENANT: `${prefix}/tenants/{tenantId}`,
  TENANT_USER: `${prefix}/tenants/{tenantId}/users/{userId}`,
  TENANT_USERS: `${prefix}/tenants/{tenantId}/users`,
  SIGNUP: `${prefix}/signup`,
  LOG: `${prefix}/_log`,
});
A case where you might want to use the routes directly is when these routes are exposed as a REST API of your application backend. For example, if you use the @niledatabase/server/express package, the routes are exposed in the app object and while you may have a frontend that uses @niledatabase/react to call these routes from the application UI, you may want to also use them as a REST API for another backend or external service. In this case, you need to use the routes directly. The key is to:
  1. Include both the session cookie and the CRSF cookie in the request headers.
  2. Include the CSRF token in the request body.
Here are a few examples of how to call the routes directly:
# This gets the CSRF token from an API exposed by your application, and also saves the cookies to a file
csrf_token=$(curl -s -X GET "http://localhost:3040/api/auth/csrf" -c "csrf_cookies.txt" | jq -r '.csrfToken')
# This signs up a new user with email/password credentials
# The CSRF token is included in the request body and in the cookies
# The cookies are saved to a file so they can be used in subsequent requests
curl -X POST "http://localhost:3040/api/signup" \
  -H "Content-Type: application/json" \
  -b "csrf_cookies.txt" \
  --cookie-jar "login_cookies.txt" \
  -d "{\"csrfToken\":\"$csrf_token\",\"email\":\"newuser@example.com\",\"password\":\"foobar\"}"
# This logs in a user with email/password credentials
# The CSRF token is included in the request body and in the cookies
# The cookies are saved to a file so they can be used in subsequent requests
curl -X POST "http://localhost:3040/api/auth/callback/credentials" \
  -H "Content-Type: application/json" \
  -b "csrf_cookies.txt" \
  --cookie-jar "login_cookies.txt" \
  -d "{\"csrfToken\":\"$csrf_token\",\"email\":\"newuser@example.com\",\"password\":\"foobar\"}"
# This gets the user's profile information
curl -X GET "http://localhost:3040/api/me" -b "login_cookies.txt"
# This creates a new tenant
curl -X POST 'localhost:3040/api/tenants' \
  -H 'Content-Type: application/json' \
  -d '{"name":"my first customer"}' \
  -b login_cookies.txt

Overriding routes

Sometimes you might want to intercept or override the routes used by the SDK in order to inject your own logic. For example, adding your own logging or metrics, adding debugging information, or perhaps injecting your own cookies during login. There are three ways to override routes:
  1. Route wrappers: Wrap the route in your own logic. This can be done in the routes file, and is useful for minor modifications or debugging.
  2. Route overrides: Override the route for a specific operation.

Route wrappers

In the examples below, we’ll use route wrappers to log the headers before every request and the body if it’s a POST request. We’ll also log the status code of the response.
// app/api/[...nile]/route.ts
import { handlers } from "./nile";

// Middleware function to log request and response details
const logRequestAndResponseDetails = (handler) => async (req, res) => {
// Log the request method and URL
console.log(`Request Method: ${req.method}, Request URL: ${req.url}`);

// Log the request headers
console.log('Request Headers:', req.headers);

// Clone the request to safely read the body
const clonedReq = req.clone();

// Log the request body if it's a POST or PUT request
if (req.method === 'POST') {
const body = await clonedReq.text();
console.log('Request Body:', body);
}
// Call the original handler and return its result
const result = await handler(req, res);

// Log the response status after the handler has executed
console.log('Result Status:', result.status);

return result;
};

// Wrap each handler with the logging middleware
export const POST = logRequestAndResponseDetails(handlers.POST);
export const GET = logRequestAndResponseDetails(handlers.GET);
export const DELETE = logRequestAndResponseDetails(handlers.DELETE);
export const PUT = logRequestAndResponseDetails(handlers.PUT);

// app/routes/nile-api.ts
// In Remix, we need to add logging on both the loader and the action
import type { Route } from "./+types/home";
import { handlers } from "~/nile";

const { GET, POST, PUT, DELETE } = handlers;

export const loader = async ({ request }: Route.LoaderArgs) => {
  // Log request headers
  console.log('Request Headers:', JSON.stringify([...request.headers]));

  let response;
  switch (request.method.toUpperCase()) {
    case "GET":
      response = await GET(request);
      break;
    case "POST":
      // Log request body for POST
      const postBody = await request.text();
      console.log('POST Request Body:', postBody);
      response = await POST(request);
      break;
    case "PUT":
      response = await PUT(request);
      break;
    case "DELETE":
      response = await DELETE(request);
      break;
    default:
      response = new Response("Method Not Allowed", { status: 405 });
  }

  // Log response status
  console.log('Response Status:', response.status);
  return response;
};

export const action = async ({ request }: Route.ActionArgs) => {
  // Log request headers
  console.log('Request Headers:', JSON.stringify([...request.headers]));

  let response;
  switch (request.method.toUpperCase()) {
    case "POST":
      // Log request body for POST
      const postBody = await request.text();
      console.log('POST Request Body:', postBody);
      response = await POST(request);
      break;
    case "PUT":
      response = await PUT(request);
      break;
    case "DELETE":
      response = await DELETE(request);
      break;
    default:
      response = new Response("Method Not Allowed", { status: 405 });
  }

  // Log response status
  console.log('Response Status:', response.status);
  return response;
};
// server.mjs
import 'dotenv/config';
import express from 'express';
import { Nile } from '@niledatabase/server';
import { NileExpressHandler } from '@niledatabase/server/express';

const startServer = async () => {
  try {
    const app = express();
    const nile = Nile({
      debug: true,
    });
    const { paths, handler } = await NileExpressHandler(nile);
    console.log(paths);

    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));

    // Create logging wrapper
    const withLogging = (handler) => async (req, res, next) => {
      console.log('Request Headers:', req.headers);
      if (req.method === 'POST') {
        console.log('Request Body:', req.body);
      }
      return handler(req, res, next);
    };

    // Apply wrapper to specific routes
    app.get(paths.get, withLogging(handler));
    app.post(paths.post, withLogging(handler));
    app.put(paths.put, withLogging(handler));
    app.delete(paths.delete, withLogging(handler));

    const PORT = process.env.PORT || 3040;
    app.listen(PORT, () => {
      console.log(`Server is running on port ${PORT}`);
    });
  } catch (error) {
    console.error('Error starting server:', error);
    process.exit(1);
  }
};

startServer();

Route overrides

In the examples below, we’ll add a new route that will override the default route for \auth\google\callback with custom logic. We are using handlersWithContext to get the nile object in the route handler. handlersWithContext returns a tuple with the nile object, configured based on the response from the route handler, and the response from the route handler.
// app/api/auth/google/callback/route.ts
import { NextRequest } from "next/server";
// make sure you export handlersWithContext from nile
import { handlersWithContext } from "../../../[...nile]/nile"; 
import { registerTenants } from "@/lib/TenantRegistration";

export async function GET(req: NextRequest) {
  // call the original route
  const {nile, response} = await handlersWithContext.GET(req);

if (nile) {
const me = await nile.users.getSelf();
if ("id" in me) {
// custom logic is here
await registerTenants(me.id);
}
}
// return the original response from the route
return response;
}

// app/routes/auth-callback-google.tsx
// Also, add to routes.ts:
// route("api/auth/google/callback", "routes/auth-callback-google.tsx")
import type { Route } from "./+types/home";
import { handlers } from "~/nile";

const { GET } = handlers;

export const loader = async ({ request }: Route.LoaderArgs) => {
  // Call Nile's GET handler
  const nileResponse = await GET(request);

  const setCookie = nileResponse.headers.get('set-cookie');
  const hasSession = setCookie && setCookie.includes("nile.session-token");

  if (hasSession) {
    // If login was successful, register the tenants
    const headers = new Headers();
    headers.append('cookie', setCookie);

    // Set the headers for the Nile API
    const nileCtx = await nile.withContext({ headers });

    // Fetch user information
    const me = await nileCtx.users.getSelf();
    if ("id" in me) {
      // Custom logic: register tenants
      await registerTenants(me.id);
    }
  }

  return nileResponse;
};
// server.mjs
// One big difference between express and other frameworks is that Nile's route handlers
// do not return a response by default. In order to use them in a custom handler,
// you need some extra configuration and extra handling of all routes

import 'dotenv/config';
import express from 'express';
import { Nile } from '@niledatabase/server';
import { NileExpressHandler } from '@niledatabase/server/express';

const startServer = async () => {
  try {
    const app = express();
    const nile = Nile({
      // debug: true,
    });
    // muteResponse is set to true to avoid sending the response back to the client from the route handler
    // this is useful when you want to override the route with your own logic
    const { paths, handler } = await NileExpressHandler(nile, {
      muteResponse: true,
    });
    console.log(paths);

    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));

    // Add custom Google OAuth callback route - This has to be done before all the default routes
    app.get('/auth/google/callback', async (req, res, next) => {
      const { status, headers, body } = await handler(req, res, next);
      if (status === 200) {
        // Fetch user information
        const nileCtx = nile.withContext({ headers });
        const me = await nileCtx.users.getSelf();
        if ('id' in me) {
          // Custom logic: register tenants
          await registerTenants(me.id);
        }
      }
      res.status(status).set(headers);
      if (typeof body === 'string') {
        res.send(body);
      } else {
        res.json(body ?? {});
      }
      return;
    });

    // Create response logging wrapper
    // This is critical to ensure that the response is sent back to the client from the route handlers
    // So make sure you use with `{ muteResponse: true }` even if you don't want to log all the responses.
    const withResponseLogging = (handler) => async (req, res, next) => {
      try {
        // Wait for the handler to complete
        const result = await handler(req, res, next);
        // Log the result
        console.log('Handler result:', result);
        const { status, headers, body } = result;
        res.status(status).set(headers);
        if (typeof body === 'string') {
          res.send(body);
        } else {
          res.json(body ?? {});
        }
        return;
      } catch (error) {
        console.error('Handler error:', error);
        throw error;
      }
    };

    // Apply wrapper to routes
    app.get(paths.get, withResponseLogging(handler));
    app.post(paths.post, withResponseLogging(handler));
    app.put(paths.put, withResponseLogging(handler));
    app.delete(paths.delete, withResponseLogging(handler));

    const PORT = process.env.PORT || 3040;
    app.listen(PORT, () => {
      console.log(`Server is running on port ${PORT}`);
    });
  } catch (error) {
    console.error('Error starting server:', error);
    process.exit(1);
  }
};

startServer();