Generated API Services Architecture
When you run the generator, Reex API Builder creates a structured api-services directory in the src folder of your project. Understanding this structure is key to leveraging the tool effectively.
Folder Structure Diagram
Here is the folder structure that gets generated:
src/
└── api-services/
├── .reex/ # Internal config & project metadata
│ ├── config.ts # Internal config & definition types
│ └── metadata.json # Project synchronization metadata
├── auth-methods/ # Modular authentication strategies & guards
├── custom/ # Custom reusable UI components
├── definitions/ # Your API implementations (editable)
├── generated/ # Auto-generated React Query hooks
├── hooks/ # Authentication & custom hooks
├── providers/ # Application providers (e.g. ReexProvider)
├── types/ # TypeScript interfaces
├── api.config.ts # Centralized user configuration
├── core.ts # Core HTTP configuration and proxy
└── ....reex/ - Internal Configuration & Metadata
Contains internal types and metadata used for project synchronization and type enforcement.
.reex/config.ts: Internal interfaces (ReexDefinition and ReexConfig).
View code
// @internal — No changes needed
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Enforces the basic structure of Reex definition files.
* Use with the `satisfies` operator for maximum IntelliSense:
* export const myApi = { ... } satisfies ReexDefinition;
*/
export type ReexDefinition = Record<string, (...args: any[]) => Promise<any>>;
/**
* Enforces the structure of the Reex configuration file (api.config.ts).
* Use with the `satisfies` operator for maximum IntelliSense.
*/
export interface ReexConfig {
baseURL: string;
unwrapResponseData: boolean;
enableApiLogging: boolean;
auth: {
loginRoute?: string;
refreshEndpoint: string;
accessTokenKey: string;
refreshTokenKey: string;
extractTokens: (responseBody: any) => { accessToken?: string; refreshToken?: string };
refreshWithCookie: () => Promise<import("axios").AxiosResponse<any>>;
refreshWithToken: (storedRefreshToken: string) => Promise<import("axios").AxiosResponse<any>>;
};
}
.reex/metadata.json: Project metadata.
View code
{
"version": "6.0.2",
"framework": "react",
"updatedAt": "2026-08-04T05:00:00.000Z"
}core.ts - Core API Client
This file contains the core Axios configuration, interceptors, and the main API client that dynamically uses the active strategy.
core.ts: Request interceptors & auth logic.
View code
// @internal — No changes needed
/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, {
AxiosError,
type AxiosInstance,
type AxiosRequestConfig,
type InternalAxiosRequestConfig,
} from "axios";
import type { ApiError, TokenProvider } from "./auth-methods/types";
import { apiConfig } from "./api.config";
import { proxyTokenProvider } from "./auth-methods/manager";
import { pushNotification } from "./hooks/notification";
const API_TIMEOUT = 30000;
const REFRESH_TIMEOUT = 10000;
// Helper to extract a human-readable string from various error response formats
const formatErrorMessage = (data: any): string | undefined => {
if (!data) return undefined;
if (typeof data.error === "string") return data.error;
if (typeof data.message === "string") return data.message;
if (typeof data.msg === "string") return data.msg;
if (data.detail !== undefined && data.detail !== null) {
if (typeof data.detail === "string") return data.detail;
if (Array.isArray(data.detail)) {
return data.detail
.map((err: any) => {
const field =
Array.isArray(err.loc) && err.loc.length
? err.loc[err.loc.length - 1]
: undefined;
const msg = err.msg || "Invalid value";
return field ? `${field}: ${msg}` : msg;
})
.join("; ");
}
if (typeof data.detail === "object") {
return data.detail.message || data.detail.msg || "Invalid request";
}
}
return undefined;
};
const DEFAULT_CONFIG: AxiosRequestConfig = {
baseURL: apiConfig.baseURL,
timeout: API_TIMEOUT,
headers: { "Content-Type": "application/json" },
};
/** Creates an Axios client with auto token injection and 401 refresh/retry */
export const createApiClient = (
tokenProvider?: TokenProvider,
): AxiosInstance => {
const client = axios.create(DEFAULT_CONFIG);
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string | null) => void;
reject: (error: any) => void;
}> = [];
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
// Request interceptor: inject token + custom headers
client.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
config.baseURL = apiConfig.baseURL;
if (config.data instanceof FormData && config.headers) {
delete config.headers["Content-Type"];
}
if (tokenProvider) {
const token = await tokenProvider.getToken();
if (token && token !== "cookie-active" && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
const customHeaders = await tokenProvider.getCustomHeaders?.();
if (customHeaders && config.headers) {
Object.entries(customHeaders).forEach(([key, value]) => {
if (typeof config.headers.set === "function") {
config.headers.set(key, value);
} else {
config.headers[key] = value;
}
});
}
}
if (apiConfig.enableApiLogging) {
console.log(
`[API Request] ${config.method?.toUpperCase()} ${config.url}`,
config.params || config.data,
);
}
return config;
},
(error) => Promise.reject(error),
);
// Response interceptor: unwrap data, handle 401 refresh, normalize errors
client.interceptors.response.use(
(response) => {
const rawData = response?.data;
const res = apiConfig.unwrapResponseData
? (rawData?.data ?? rawData)
: rawData;
if (apiConfig.enableApiLogging) {
console.log(
`[API Success] ${response.config.method?.toUpperCase()} ${response.config.url}`,
res,
);
}
return res;
},
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
// Handle 401 with token refresh
if (
error.response?.status === 401 &&
!originalRequest._retry &&
tokenProvider?.refreshToken
) {
if (isRefreshing) {
return new Promise<string | null>((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(
new Error("Token refresh timeout - request took too long"),
);
}, REFRESH_TIMEOUT);
failedQueue.push({
resolve: (token) => {
clearTimeout(timeoutId);
resolve(token);
},
reject: (err) => {
clearTimeout(timeoutId);
reject(err);
},
});
})
.then((token) => {
if (token && originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${token}`;
}
return client(originalRequest);
})
.catch((err) => Promise.reject(err));
}
originalRequest._retry = true;
isRefreshing = true;
try {
const newToken = await tokenProvider.refreshToken();
if (newToken && originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newToken}`;
processQueue(null, newToken);
return client(originalRequest);
}
throw new Error("Refresh failed to return a valid token");
} catch (refreshError) {
processQueue(refreshError, null);
// Refresh failed — signal the app to log the user out and clear the session.
if (typeof window !== "undefined") {
window.dispatchEvent(new Event("auth:logout"));
}
// Notify the user. To redirect instead of showing a toast, handle "auth:logout" in your auth guard.
pushNotification({
type: "error",
message:
formatErrorMessage((error as any).response?.data) ||
"Authentication failed. Please log in again.",
statusCode: 401,
});
// Return early so the generic error handler below does not also fire a notification.
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
} else if (error.response?.status === 401) {
// No token refresh configured — log the user out immediately.
if (typeof window !== "undefined") {
window.dispatchEvent(new Event("auth:logout"));
}
pushNotification({
type: "error",
message:
formatErrorMessage(error.response?.data) ||
"Authentication failed. Please log in again.",
statusCode: 401,
});
// Return early so the generic error handler below does not also fire a notification.
throw error;
}
// Normalize the raw Axios error into a consistent ApiError shape.
const responseData = error.response?.data as any;
const apiError: ApiError = {
message:
formatErrorMessage(responseData) ||
error.message ||
"An unexpected error occurred",
code: responseData?.error || responseData?.code,
statusCode: error.response?.status,
originalError: error,
};
if (originalRequest) {
const endpoint = `${originalRequest.method?.toUpperCase()} ${originalRequest.url}`;
console.log(`[API ERROR - ${endpoint}]`, {
message: apiError.message,
code: apiError.code,
statusCode: apiError.statusCode,
});
}
// Surface the error to the user. Customize the UI in AppNotification.tsx.
pushNotification({
type: "error",
message: apiError.message,
statusCode: apiError.statusCode,
});
throw apiError;
},
);
return client;
};
/**
* The globally configured API client instance.
* It dynamically uses the Active Strategy defined in the AuthProvider.
*/
export const apiClient = createApiClient(proxyTokenProvider);auth-methods/ - Authentication Strategies
Provides modular authentication strategies and guards.
AuthProvider.tsx: Sets the global strategy and provides the guards.
View code
"use client";
import React, { useState } from "react";
import { CookieAuthGuard } from "./cookie-auth/CookieAuthGuard";
import { NextAuthGuard } from "./next-auth/NextAuthGuard";
import { JwtAuthGuard } from "./jwt-auth/JwtAuthGuard";
import { setActiveStrategy, type AuthStrategy } from "./manager";
interface AuthProviderProps {
strategy: AuthStrategy;
children: React.ReactNode;
}
/**
* Global AuthProvider that wraps the application and conditionally renders
* the appropriate AuthGuard based on the configured strategy.
* It also configures the global API client to use the correct TokenProvider.
*/
export const AuthProvider = ({ strategy, children }: AuthProviderProps) => {
// Synchronously configure the API client's Strategy on the first render
useState(() => {
setActiveStrategy(strategy);
return true;
});
switch (strategy) {
case "cookie":
return <CookieAuthGuard>{children}</CookieAuthGuard>;
case "next-auth":
return <NextAuthGuard>{children}</NextAuthGuard>;
case "jwt":
return <JwtAuthGuard>{children}</JwtAuthGuard>;
default:
console.warn(`Unknown auth strategy: ${strategy}. Falling back to jwt.`);
return <JwtAuthGuard>{children}</JwtAuthGuard>;
}
};manager.ts: Strategy switcher and dynamic proxy.
View code
import { cookieTokenProvider } from "./cookie-auth/provider";
import { jwtTokenProvider } from "./jwt-auth/provider";
import { nextAuthTokenProvider } from "./next-auth/provider";
import { type TokenProvider } from "./types";
export type AuthStrategy = "jwt" | "cookie" | "next-auth";
let activeTokenProvider: TokenProvider = jwtTokenProvider;
/**
* Updates the globally active token provider based on the chosen strategy.
* Used internally by the <AuthProvider> component.
*/
export const setActiveStrategy = (strategy: AuthStrategy) => {
switch (strategy) {
case "cookie":
activeTokenProvider = cookieTokenProvider;
break;
case "next-auth":
activeTokenProvider = nextAuthTokenProvider;
break;
case "jwt":
activeTokenProvider = jwtTokenProvider;
break;
}
};
/**
* Proxy provider that dynamically resolves to the currently active token provider.
* This ensures the API client always uses the correctly configured strategy without
* needing to recreate the Axios instance.
*/
export const proxyTokenProvider: TokenProvider = {
getToken: () => activeTokenProvider.getToken(),
get refreshToken() {
return activeTokenProvider.refreshToken;
},
get setTokens() {
return activeTokenProvider.setTokens;
},
get setSession() {
return activeTokenProvider.setSession;
},
get clearTokens() {
return activeTokenProvider.clearTokens;
},
get getCustomHeaders() {
return activeTokenProvider.getCustomHeaders;
},
get setCustomHeaders() {
return activeTokenProvider.setCustomHeaders;
},
get removeCustomHeader() {
return activeTokenProvider.removeCustomHeader;
},
};
/**
* Use this function to access the active token provider from an external file.
* This is the recommended way to get or set auth tokens after login.
*/
export function getActiveProvider(): TokenProvider | null {
return activeTokenProvider ?? null;
}types.ts: Auth-related interfaces.
View code
/** Standardized API error response */
export interface ApiError {
message: string;
code?: string;
statusCode?: number;
originalError?: unknown;
}
/** Pluggable authentication strategy */
export interface TokenProvider {
/** Get current access token */
getToken: () => Promise<string | null> | string | null;
/** Refresh expired token */
refreshToken?: () => Promise<string | null>;
/** Return custom headers to inject into all requests */
getCustomHeaders?: () =>
| Promise<Record<string, string>>
| Record<string, string>;
/** Replace custom headers for all future requests */
setCustomHeaders?: (headers: Record<string, string>) => void;
/** Remove a specific custom header */
removeCustomHeader?: (key: string) => void;
/** Set new auth tokens (called on login/signup for token-based auth) */
setTokens?: (tokens: { accessToken: string; refreshToken?: string }) => void;
/** Activate or set session on login (handles tokens, cookie-based sessions, and optional headers) */
setSession?: (
tokens?: { accessToken?: string; refreshToken?: string },
headers?: Record<string, string>,
) => void;
/** Clear all auth tokens (called on logout) */
clearTokens?: () => void;
}Cookie Auth Strategy
View code
// @internal — No changes needed
"use client";
import { useEffect, useState } from "react";
import { cookieTokenProvider } from "./provider";
import { apiClient } from "../../../api-services/core";
/**
* Restores the user session on mount via the refresh endpoint.
*/
export const CookieAuthGuard = ({
children,
}: {
children: React.ReactNode;
}) => {
// Configure API client synchronously on mount
useState(() => {
apiClient.defaults.withCredentials = true;
return true;
});
useEffect(() => {
// Attempt to restore access token from refresh cookie or localStorage
const initAuth = async () => {
try {
await cookieTokenProvider.refreshToken?.();
} catch (error) {
console.warn("Auth initialization failed:", error);
}
};
initAuth();
}, []);
return <>{children}</>;
};// @internal — No changes needed
import { type TokenProvider } from "../types";
import { apiConfig } from "../../api.config";
let isCookieAuthActive: boolean = false;
let customHeaders: Record<string, string> | null = null;
let refreshPromise: Promise<string | null> | null = null;
// Used to track auth status across tabs, without exposing the actual token to JS
const COOKIE_FLAG_KEY = "reex_is_cookie_auth_active";
interface CookieTokenProvider extends TokenProvider {
setSession: (
tokens?: { accessToken?: string; refreshToken?: string },
headers?: Record<string, string>,
) => void;
clearTokens: () => void;
setCustomHeaders: (headers: Record<string, string>) => void;
getCustomHeaders: () => Record<string, string>;
removeCustomHeader: (key: string) => void;
}
/**
* Cookie-Only Token Provider
* - Access Token: Managed securely by the browser via HttpOnly cookies
* - Refresh Mechanism: Automatic via HttpOnly cookies
* - Frontend State: Maintained via a simple boolean flag
*/
export const cookieTokenProvider: CookieTokenProvider = {
getToken: () => {
// Return a proxy value so `useIsAuthenticated` knows we are logged in.
// The actual token is sent automatically by the browser via cookies.
if (isCookieAuthActive) return "cookie-active";
if (typeof window !== "undefined") {
return localStorage.getItem(COOKIE_FLAG_KEY) === "true" ? "cookie-active" : null;
}
return null;
},
getCustomHeaders: () => {
if (customHeaders) return customHeaders;
if (typeof window !== "undefined") {
const stored = localStorage.getItem("reex_custom_headers");
if (stored) {
try {
return JSON.parse(stored);
} catch {
return {};
}
}
}
return {};
},
setCustomHeaders: (headers) => {
customHeaders = headers;
if (typeof window !== "undefined") {
localStorage.setItem("reex_custom_headers", JSON.stringify(headers));
}
},
removeCustomHeader: (key) => {
if (customHeaders) {
delete customHeaders[key];
}
if (typeof window !== "undefined") {
const stored = localStorage.getItem("reex_custom_headers");
if (stored) {
try {
const headers = JSON.parse(stored);
delete headers[key];
localStorage.setItem("reex_custom_headers", JSON.stringify(headers));
} catch {
// ignore JSON parse / storage errors
}
}
}
},
setSession: (_tokens, headers) => {
// The actual token is set by the backend via Set-Cookie headers.
// We just set a primitive flag so the frontend knows the user is logged in.
isCookieAuthActive = true;
if (headers) {
cookieTokenProvider.setCustomHeaders(headers);
}
if (typeof window !== "undefined") {
localStorage.setItem(COOKIE_FLAG_KEY, "true");
window.dispatchEvent(new Event("auth:login"));
}
},
clearTokens: () => {
isCookieAuthActive = false;
customHeaders = null;
refreshPromise = null;
if (typeof window !== "undefined") {
localStorage.removeItem(COOKIE_FLAG_KEY);
localStorage.removeItem("reex_custom_headers");
}
},
refreshToken: async () => {
if (typeof window !== "undefined" && localStorage.getItem(COOKIE_FLAG_KEY) !== "true") {
return null;
}
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = (async () => {
try {
await apiConfig.auth.refreshWithCookie();
// If the refresh succeeds, the browser now has the updated cookie.
isCookieAuthActive = true;
if (typeof window !== "undefined") {
localStorage.setItem(COOKIE_FLAG_KEY, "true");
window.dispatchEvent(new Event("auth:login"));
}
return "cookie-active";
} catch {
// Refresh failed, clear the session flag
isCookieAuthActive = false;
if (typeof window !== "undefined") {
localStorage.removeItem(COOKIE_FLAG_KEY);
}
return null;
} finally {
refreshPromise = null;
}
})();
return refreshPromise;
},
};JWT (Token) Strategy
View code
// @internal — No changes needed
"use client";
import { useEffect } from "react";
import { jwtTokenProvider } from "./provider";
import { apiConfig } from "../../api.config";
/**
* Restores the user session on mount by exchanging the stored refresh token.
*/
export const JwtAuthGuard = ({
children,
}: {
children: React.ReactNode;
}) => {
useEffect(() => {
// Attempt to restore session from stored tokens
const initAuth = async () => {
const hasRefreshToken = !!localStorage.getItem(
apiConfig.auth.refreshTokenKey,
);
const hasAccessToken = !!localStorage.getItem(
apiConfig.auth.accessTokenKey,
);
if ((hasRefreshToken || hasAccessToken) && jwtTokenProvider?.refreshToken) {
try {
await jwtTokenProvider.refreshToken();
} catch (error) {
console.warn("Session restoration failed:", error);
}
}
};
initAuth();
}, []);
return <>{children}</>;
};// @internal — No changes needed
import { apiConfig } from "../../api.config";
import { type TokenProvider } from "../types";
/**
* JWT Token Provider
* - Access Token & Refresh Token: Persisted in localStorage
*
* Edit `api.config.ts` to customize routes and storage keys, and refresh payload.
*/
let accessToken: string | null = null;
let customHeaders: Record<string, string> | null = null;
let refreshPromise: Promise<string | null> | null = null;
/**
* Decodes the JWT `exp` claim and checks whether it has passed.
* No library needed — JWT payloads are plain base64url-encoded JSON.
* Returns false for non-JWT strings so opaque tokens are never blocked.
*/
const isTokenExpired = (token: string): boolean => {
try {
const payload = JSON.parse(atob(token.split(".")[1]));
return typeof payload.exp === "number" && payload.exp * 1000 < Date.now();
} catch {
return false;
}
};
interface JwtTokenProvider extends TokenProvider {
setTokens: (tokens: { accessToken: string; refreshToken?: string }) => void;
setSession: (
tokens?: { accessToken?: string; refreshToken?: string },
headers?: Record<string, string>,
) => void;
clearTokens: () => void;
setCustomHeaders: (headers: Record<string, string>) => void;
getCustomHeaders: () => Record<string, string>;
removeCustomHeader: (key: string) => void;
}
export const jwtTokenProvider: JwtTokenProvider = {
getToken: () => {
const token = accessToken ?? (
typeof window !== "undefined"
? localStorage.getItem(apiConfig.auth.accessTokenKey)
: null
);
if (token) {
if (isTokenExpired(token)) {
accessToken = null;
if (typeof window !== "undefined") {
localStorage.removeItem(apiConfig.auth.accessTokenKey);
const hasRefreshToken = !!localStorage.getItem(apiConfig.auth.refreshTokenKey);
if (!hasRefreshToken) {
window.dispatchEvent(new Event("auth:logout"));
return null;
}
}
} else {
return token;
}
}
if (typeof window !== "undefined") {
const storedRefreshToken = localStorage.getItem(apiConfig.auth.refreshTokenKey);
if (storedRefreshToken && jwtTokenProvider.refreshToken) {
return jwtTokenProvider.refreshToken();
}
}
return null;
},
getCustomHeaders: () => {
if (customHeaders) return customHeaders;
if (typeof window !== "undefined") {
const stored = localStorage.getItem("reex_custom_headers");
if (stored) {
try {
return JSON.parse(stored);
} catch {
return {};
}
}
}
return {};
},
setCustomHeaders: (headers) => {
customHeaders = headers;
if (typeof window !== "undefined") {
localStorage.setItem("reex_custom_headers", JSON.stringify(headers));
}
},
removeCustomHeader: (key) => {
if (customHeaders) {
delete customHeaders[key];
}
if (typeof window !== "undefined") {
const stored = localStorage.getItem("reex_custom_headers");
if (stored) {
try {
const headers = JSON.parse(stored);
delete headers[key];
localStorage.setItem("reex_custom_headers", JSON.stringify(headers));
} catch {
// ignore JSON parse / storage errors
}
}
}
},
// Called on login/signup success with raw tokens
setTokens: (tokens) => {
const newAccessToken = tokens.accessToken;
const refreshToken = tokens.refreshToken;
accessToken = newAccessToken;
if (typeof window !== "undefined") {
if (refreshToken) {
localStorage.setItem(apiConfig.auth.refreshTokenKey, refreshToken);
localStorage.removeItem(apiConfig.auth.accessTokenKey);
} else if (newAccessToken) {
localStorage.setItem(apiConfig.auth.accessTokenKey, newAccessToken);
localStorage.removeItem(apiConfig.auth.refreshTokenKey);
}
window.dispatchEvent(new Event("auth:login"));
}
},
// Called on login with tokens and optional custom headers
setSession: (tokens, headers) => {
if (tokens?.accessToken) {
jwtTokenProvider.setTokens(tokens as { accessToken: string; refreshToken?: string });
}
if (headers) {
jwtTokenProvider.setCustomHeaders(headers);
}
if (typeof window !== "undefined" && !tokens?.accessToken) {
window.dispatchEvent(new Event("auth:login"));
}
},
// Called on logout
clearTokens: () => {
accessToken = null;
customHeaders = null;
refreshPromise = null;
if (typeof window !== "undefined") {
localStorage.removeItem(apiConfig.auth.refreshTokenKey);
localStorage.removeItem(apiConfig.auth.accessTokenKey);
localStorage.removeItem("reex_custom_headers");
}
},
// Exchange refresh token for a new access token
refreshToken: async () => {
// If a refresh is already in progress, await the existing promise instead of making a new request
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = (async () => {
try {
if (typeof window === "undefined") return null;
const storedRefreshToken = localStorage.getItem(
apiConfig.auth.refreshTokenKey,
);
// Fallback: use stored access token if no refresh token
if (!storedRefreshToken) {
const storedAccessToken = localStorage.getItem(
apiConfig.auth.accessTokenKey,
);
if (storedAccessToken) {
accessToken = storedAccessToken;
return accessToken;
}
throw new Error("No refresh token found");
}
const response = await apiConfig.auth.refreshWithToken(storedRefreshToken);
const isRawAxiosResponse =
response?.config && response?.headers && response?.status;
const unwrappedData = isRawAxiosResponse ? response.data : response;
const responseData = apiConfig.unwrapResponseData ? (unwrappedData?.data ?? unwrappedData) : unwrappedData;
const extracted = apiConfig.auth.extractTokens(responseData);
if (!extracted?.accessToken && !extracted?.refreshToken) {
console.error(
"\n[Reex Auth Error] 🛑 Token extraction failed during refresh.\n" +
"Both 'accessToken' and 'refreshToken' are empty.\n" +
"Please update the `extractTokens` function in your `api.config.ts` so it correctly maps to your backend's response structure.\n"
);
}
const { accessToken: newAccess = null, refreshToken: newRefresh } = extracted || {};
accessToken = newAccess;
if (newRefresh) {
localStorage.setItem(apiConfig.auth.refreshTokenKey, newRefresh);
}
if (newAccess && typeof window !== "undefined") {
window.dispatchEvent(new Event("auth:login"));
}
return accessToken;
} catch {
console.warn("Refresh failed, logging out...");
accessToken = null;
localStorage.removeItem(apiConfig.auth.refreshTokenKey);
localStorage.removeItem(apiConfig.auth.accessTokenKey);
return null;
} finally {
refreshPromise = null;
}
})();
return refreshPromise;
},
};NextAuth Strategy
View code
// @internal — No changes needed
"use client";
import { SessionProvider } from "next-auth/react";
import { useEffect } from "react";
/**
* Authentication guard wrapper for NextAuth.js
*/
export const NextAuthGuard = ({ children }: { children: React.ReactNode }) => {
return (
<SessionProvider>
<NextAuthGuardInner>{children}</NextAuthGuardInner>
</SessionProvider>
);
};
const NextAuthGuardInner = ({ children }: { children: React.ReactNode }) => {
// Listen for auth:logout events dispatched by core.ts when token refresh fails
useEffect(() => {
const handleLogout = () => {
import("next-auth/react").then(({ signOut }) => signOut());
};
window.addEventListener("auth:logout", handleLogout);
return () => window.removeEventListener("auth:logout", handleLogout);
}, []);
return <>{children}</>;
};// @internal — No changes needed
import { type TokenProvider } from "../types";
import { getSession } from "next-auth/react";
// Extend the Session type to include accessToken
declare module "next-auth" {
interface Session {
accessToken?: string;
}
}
let customHeaders: Record<string, string> | null = null;
// TokenProvider for NextAuth.js - retrieves token from session
interface NextAuthTokenProvider extends TokenProvider {
setTokens?: (tokens: { accessToken: string; refreshToken?: string }) => void;
setSession?: (
tokens?: { accessToken?: string; refreshToken?: string },
headers?: Record<string, string>,
) => void;
clearTokens: () => void;
setCustomHeaders: (headers: Record<string, string>) => void;
getCustomHeaders: () => Record<string, string>;
}
export const nextAuthTokenProvider: NextAuthTokenProvider = {
getToken: async () => {
const session = await getSession();
return (session?.accessToken as string) || null;
},
getCustomHeaders: () => {
if (customHeaders) return customHeaders;
if (typeof window !== "undefined") {
const stored = localStorage.getItem("reex_custom_headers");
if (stored) {
try {
return JSON.parse(stored);
} catch {
return {};
}
}
}
return {};
},
setCustomHeaders: (headers) => {
customHeaders = headers;
if (typeof window !== "undefined") {
localStorage.setItem("reex_custom_headers", JSON.stringify(headers));
}
},
refreshToken: undefined,
// Placeholder methods for compatibility with other providers
// NextAuth manages these internally via session
setTokens: () => {
if (process.env.NODE_ENV === "development") {
console.warn(
"[NextAuthTokenProvider] setTokens is a no-op. NextAuth manages tokens via session.",
);
}
},
setSession: (_tokens, headers) => {
if (headers) {
nextAuthTokenProvider.setCustomHeaders(headers);
}
},
clearTokens: () => {
customHeaders = null;
if (typeof window !== "undefined") {
localStorage.removeItem("reex_custom_headers");
}
if (process.env.NODE_ENV === "development") {
console.warn(
"[NextAuthTokenProvider] clearTokens token part is a no-op. Use signOut() from next-auth/react instead.",
);
}
},
};hooks/ - App State & Notifications
Provides centralized application state management, particularly for notifications.
notification.ts: Notification context and standalone dispatcher.
View code
"use client";
import { createContext } from "react";
export type NotificationType = "success" | "error" | "warning" | "info";
export interface ApiNotification {
id: string;
type: NotificationType;
message: string;
statusCode?: number;
timestamp: string;
}
export const NOTIFICATION_EVENT = "api:notification" as const;
// Suppresses duplicate toasts when the same error fires multiple times in quick succession.
const DEDUPE_WINDOW_MS = 2500;
const recentNotifications = new Map<string, ReturnType<typeof setTimeout>>();
/**
* Pushes an error or status notification outside of React (e.g. from API service files).
* Identical notifications within 2.5 s are suppressed to prevent duplicate toasts.
*
* @example
* pushNotification({ type: "error", message: "Failed to load profile", statusCode: 500 });
*/
export const pushNotification = (
notification: Omit<ApiNotification, "id" | "timestamp">,
) => {
if (typeof window === "undefined") return;
const dedupeKey = `${notification.type}:${notification.statusCode ?? ""}:${notification.message}`;
if (recentNotifications.has(dedupeKey)) return;
// Auto-expire the key so the same error can reappear after the window closes.
const timer = setTimeout(() => recentNotifications.delete(dedupeKey), DEDUPE_WINDOW_MS);
recentNotifications.set(dedupeKey, timer);
window.dispatchEvent(
new CustomEvent(NOTIFICATION_EVENT, {
detail: {
...notification,
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
},
}),
);
};
export interface NotificationContextValue {
notifications: ApiNotification[];
dismiss: (id: string) => void;
dismissAll: () => void;
pushNotification: (
notification: Omit<ApiNotification, "id" | "timestamp">,
) => void;
}
export const NotificationContext = createContext<NotificationContextValue | null>(
null,
);useNotification.ts: Consumer hook for notifications.
View code
import { useContext } from "react";
import { NotificationContext } from "./notification";
/**
* Hook to consume the centralized notification state.
* Must be used within a <NotificationProvider>.
*/
export const useNotification = () => {
const context = useContext(NotificationContext);
if (!context) {
throw new Error(
"useNotification must be used within a NotificationProvider",
);
}
return context;
};providers/ - Application Providers
ReexProvider.tsx: Sets the global strategy and provides the guards and notifications.
View code
// @internal — No changes needed
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type ReactNode, useState } from "react";
import { AuthProvider } from "../auth-methods/AuthProvider";
import type { AuthStrategy } from "../auth-methods/manager";
import AppNotification from "../custom/notification/AppNotification/AppNotification";
import { NotificationProvider } from "./NotificationProvider";
export function ReexProvider({
children,
strategy,
}: {
children: ReactNode;
strategy: AuthStrategy;
}) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<NotificationProvider>
<AuthProvider strategy={strategy}>{children}</AuthProvider>
<AppNotification />
</NotificationProvider>
</QueryClientProvider>
);
}NotificationProvider.tsx: Notification context provider.
View code
// @internal — No changes needed
import {
type ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
type ApiNotification,
NOTIFICATION_EVENT,
pushNotification,
NotificationContext,
type NotificationContextValue,
} from "../hooks/notification";
export interface UseNotificationOptions {
maxNotifications?: number;
autoDismissMs?: number | false;
}
const DEFAULT_MAX = 10;
const DEFAULT_AUTO_DISMISS_MS = 5000;
export const NotificationProvider = ({
children,
options,
}: {
children: ReactNode;
options?: UseNotificationOptions;
}) => {
const {
maxNotifications = DEFAULT_MAX,
autoDismissMs = DEFAULT_AUTO_DISMISS_MS,
} = options ?? {};
const [notifications, setNotifications] = useState<ApiNotification[]>([]);
const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(
new Map(),
);
const dismiss = useCallback((id: string) => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
const timer = timersRef.current.get(id);
if (timer) {
clearTimeout(timer);
timersRef.current.delete(id);
}
}, []);
const dismissAll = useCallback(() => {
setNotifications([]);
timersRef.current.forEach((timer) => clearTimeout(timer));
timersRef.current.clear();
}, []);
useEffect(() => {
if (typeof window === "undefined") return;
// Captured for safe cleanup in React Strict Mode
const timers = timersRef.current;
const handleNotification = (event: Event) => {
const notification = (event as CustomEvent<ApiNotification>).detail;
if (!notification?.id) return;
setNotifications((prev) => {
if (prev.some((n) => n.id === notification.id)) {
return prev;
}
// Drop duplicates already on screen (guards against same-render-batch bursts).
if (prev.some((n) => n.message === notification.message && n.type === notification.type)) {
return prev;
}
const next = [...prev, notification];
if (next.length <= maxNotifications) {
return next;
}
// Remove timers for notifications that are being evicted
const removed = next.slice(0, next.length - maxNotifications);
// Defer timer cleanup to keep the state updater pure.
queueMicrotask(() => {
removed.forEach(({ id }) => {
const timer = timers.get(id);
if (timer) {
clearTimeout(timer);
timers.delete(id);
}
});
});
return next.slice(next.length - maxNotifications);
});
if (autoDismissMs !== false) {
const timer = setTimeout(() => {
dismiss(notification.id);
}, autoDismissMs);
timers.set(notification.id, timer);
}
};
window.addEventListener(NOTIFICATION_EVENT, handleNotification);
return () => {
window.removeEventListener(NOTIFICATION_EVENT, handleNotification);
timers.forEach((timer) => clearTimeout(timer));
timers.clear();
};
}, [maxNotifications, autoDismissMs, dismiss]);
const contextValue = useMemo<NotificationContextValue>(
() => ({
notifications,
dismiss,
dismissAll,
pushNotification,
}),
[notifications, dismiss, dismissAll],
);
return (
<NotificationContext.Provider value={contextValue}>
{children}
</NotificationContext.Provider>
);
};definitions/ - REST API Endpoints
This folder contains sample API endpoint definitions.
- Editable: You can make changes to these files to match your API.
- Two-Way Sync: Changes here are reflected in the Reex API Builder UI.
Note on File Uploads: Instead of manually setting the header inside the function, the recommended approach is to use the
@contentType multipart/form-dataJSDoc tag above the endpoint.
- The Reex UI uses this tag to generate the correct file upload interface.
- Passing a
FormDataobject directly allows Axios to automatically calculate and append the necessaryboundary=string. Manually setting the header can cause backend parsing errors!/** * Upload user photo * @auth * @contentType multipart/form-data */ uploadPhoto: async (payload: FormData) => { return await apiClient.post(`/user/profile/photo`, payload); }
authentication.ts: Authentication endpoints (Refresh, Signup, Login, Logout).
View code
import { apiClient } from "../core";
import { type ReexDefinition } from "../.reex/config";
import { type post_authRefresh } from "../types/authentication/post_authRefresh";
import { type post_authSignup } from "../types/authentication/post_authSignup";
import { type post_authLogin } from "../types/authentication/post_authLogin";
import { type post_authLogout } from "../types/authentication/post_authLogout";
export interface AuthSignupPayload {
username: string;
password: string;
}
export interface AuthLoginPayload {
username: string;
password: string;
}
export const authenticationApi = {
/** @description Refresh access token */
post_authRefresh: (): Promise<post_authRefresh> =>
apiClient.post(`/auth/refresh`, {}),
/** @description Register a new user */
post_authSignup: (payload: AuthSignupPayload): Promise<post_authSignup> =>
apiClient.post(`/auth/signup`, payload),
/** @description Login a user */
post_authLogin: (payload: AuthLoginPayload): Promise<post_authLogin> =>
apiClient.post(`/auth/login`, payload),
/** @description Logout a user */
post_authLogout: (): Promise<post_authLogout> =>
apiClient.post(`/auth/logout`, {})
} satisfies ReexDefinition;todos.ts: CRUD operations for Todos.
View code
/* eslint-disable @typescript-eslint/no-explicit-any */
import { apiClient } from "../core";
import { type ReexDefinition } from "../.reex/config";
import { type get_todos } from "../types/todos/get_todos";
import { type post_todos } from "../types/todos/post_todos";
import { type put_todos } from "../types/todos/put_todos";
export interface TodosPayload {
task: string;
priority?: number;
}
export const todosApi = {
/** @description Retrieve a list of todos for the authenticated user @auth */
get_todos: (): Promise<get_todos> =>
apiClient.get(`/todos`),
/** @description Create a new todo @auth */
post_todos: (payload: TodosPayload): Promise<post_todos> =>
apiClient.post(`/todos`, payload),
/** @description Update a todo @auth @param id The todo ID */
put_todos: (id: string): Promise<put_todos> =>
apiClient.put(`/todos/${id}`, {}),
/** @description Delete a todo @auth @param id The todo ID */
delete_todos: (id: string): Promise<void> =>
apiClient.delete(`/todos/${id}`)
} satisfies ReexDefinition;generated/ - React Query Hooks
Auto-generated hooks based on your definitions/.
query.config.ts: React Query wrapper hooks. Edit this file to configure defaults like refetchOnWindowFocus.
View code
// @user-config — customize query defaults here
import {
type QueryKey,
type UseMutationOptions,
type UseQueryOptions,
type UseQueryResult,
type DefaultError,
useMutation,
useQuery,
} from "@tanstack/react-query";
import { getActiveProvider } from "../auth-methods/manager";
// 1. Mutation Wrapper
export const useApiMutation = <
TData = unknown,
TVariables = void,
TError = DefaultError,
TContext = unknown,
>(
mutationFn: (variables: TVariables) => Promise,
options?: Omit<
UseMutationOptions<TData, TError, TVariables, TContext>,
"mutationFn"
>,
) => {
return useMutation<TData, TError, TVariables, TContext>({
mutationFn,
...options,
});
};
// 2. Query Wrapper
export const useApiQuery = <
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
queryKey: TQueryKey,
queryFn: () => Promise,
options?: Omit<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
"queryKey" | "queryFn"
>,
): UseQueryResult<TData, TError> => {
const isEnabled =
options?.enabled !== false ? !!getActiveProvider()?.getToken?.() : false;
return useQuery<TQueryFnData, TError, TData, TQueryKey>({
queryKey,
queryFn,
refetchOnWindowFocus: false,
// Skip retrying auth/client errors (401, 403, 404, 422) — these won't resolve on retry.
retry: (failureCount: number, error: unknown) => {
const e = error as { statusCode?: number; status?: number; response?: { status?: number } };
const status = e?.statusCode ?? e?.status ?? e?.response?.status;
if (status === 401 || status === 403 || status === 404 || status === 422) {
return false;
}
return failureCount < 1;
},
...options,
enabled: isEnabled,
});
};index.ts: Barrel exports.
View code
// Generated file - DO NOT EDIT
export * from "./useAuthenticationQueries";
export * from "./useTodosQueries";useAuthenticationQueries.ts: Hooks for Authentication.
View code
/* eslint-disable @typescript-eslint/no-explicit-any */
// Generated file - DO NOT EDIT
import { useQueryClient, type UseMutationOptions } from "@tanstack/react-query";
import { authenticationApi } from "../definitions/authentication";
import { useApiMutation } from "./query.config";
// Helper Types
type ApiData any> = Awaited<ReturnType>;
type ApiVars any> = Parameters[0];
export const authenticationKeys = {
all: ["authentication"] as const,
};
export const usePostAuthSignupMutation = (
options?: Omit<
UseMutationOptions<ApiData, Error, ApiVars>,
"mutationFn"
> & { invalidate?: boolean }
) => {
const queryClient = useQueryClient();
return useApiMutation(authenticationApi.post_authSignup, {
...options,
onSuccess: (data, variables, context) => {
if (options?.invalidate !== false) {
queryClient.invalidateQueries({ queryKey: authenticationKeys.all });
}
(options?.onSuccess as any)?.(data, variables, context);
},
});
};
export const usePostAuthLoginMutation = (
options?: Omit<
UseMutationOptions<ApiData, Error, ApiVars>,
"mutationFn"
> & { invalidate?: boolean }
) => {
const queryClient = useQueryClient();
return useApiMutation(authenticationApi.post_authLogin, {
...options,
onSuccess: (data, variables, context) => {
if (options?.invalidate !== false) {
queryClient.invalidateQueries({ queryKey: authenticationKeys.all });
}
(options?.onSuccess as any)?.(data, variables, context);
},
});
};
export const usePostAuthLogoutMutation = (
options?: Omit<
UseMutationOptions<ApiData, Error, ApiVars>,
"mutationFn"
> & { invalidate?: boolean }
) => {
const queryClient = useQueryClient();
return useApiMutation(authenticationApi.post_authLogout, {
...options,
onSuccess: (data, variables, context) => {
if (options?.invalidate !== false) {
queryClient.invalidateQueries({ queryKey: authenticationKeys.all });
}
(options?.onSuccess as any)?.(data, variables, context);
},
});
};useTodosQueries.ts: Hooks for Todos.
View code
/* eslint-disable @typescript-eslint/no-explicit-any */
// Generated file - DO NOT EDIT
import { useQueryClient, type UseMutationOptions, type UseQueryOptions } from "@tanstack/react-query";
import { todosApi } from "../definitions/todos";
import { useApiQuery, useApiMutation } from ".";
// Helper Types
type ApiData any> = Awaited<ReturnType>;
type ApiVars any> = Parameters[0];
export const todosKeys = {
all: ["todos"] as const,
get_todos: (params?: ApiVars) => [...todosKeys.all, "get_todos", params] as const,
};
export const useGetTodosQuery = >(
options?: Omit<
UseQueryOptions<ApiData, Error, TData>,
"queryKey" | "queryFn"
>
) =>
useApiQuery(
todosKeys.get_todos(),
() => todosApi.get_todos(),
options
);
export const usePostTodosMutation = (
options?: Omit<
UseMutationOptions<ApiData, Error, ApiVars>,
"mutationFn"
> & { invalidate?: boolean }
) => {
const queryClient = useQueryClient();
return useApiMutation(todosApi.post_todos, {
...options,
onSuccess: (data, variables, context) => {
if (options?.invalidate !== false) {
queryClient.invalidateQueries({ queryKey: todosKeys.all });
}
(options?.onSuccess as any)?.(data, variables, context);
},
});
};hooks/ - Custom Hooks
useAuthSession.ts: Utility hook for useAuthSession.
// @internal — No changes needed
import { useCallback, useEffect, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getActiveProvider } from "../auth-methods/manager";
import { apiConfig } from "../api.config";
export type AuthStatus = "loading" | "authenticated" | "unauthenticated";
/**
* Hook to manage authentication state, active sessions, and logout teardown.
*
* @example
* const { status, isAuthenticated, setSession, clearSession } = useAuthSession();
* if (status === "loading") return <Loading />;
* if (!isAuthenticated) return <Login />;
*/
export const useAuthSession = () => {
const provider = getActiveProvider();
const queryClient = useQueryClient();
const [status, setStatus] = useState<AuthStatus>("loading");
useEffect(() => {
const checkAuth = async () => {
try {
const token = await provider?.getToken?.();
setStatus(token ? "authenticated" : "unauthenticated");
} catch {
setStatus("unauthenticated");
}
};
checkAuth();
// Listen to dispatch events to update UI instantly without refresh
const handleLogin = () => setStatus("authenticated");
const handleLogout = () => {
provider?.clearTokens?.();
queryClient.cancelQueries();
queryClient.clear();
setStatus("unauthenticated");
if (typeof window !== "undefined") {
const route = apiConfig.auth?.loginRoute || window.origin;
window.location.href = route;
}
};
if (typeof window !== "undefined") {
window.addEventListener("auth:login", handleLogin);
window.addEventListener("auth:logout", handleLogout);
}
return () => {
if (typeof window !== "undefined") {
window.removeEventListener("auth:login", handleLogin);
window.removeEventListener("auth:logout", handleLogout);
}
};
}, [provider, queryClient]);
const getToken = useCallback(
() => provider?.getToken?.() ?? null,
[provider],
);
const setSession = useCallback(
(
tokens?: { accessToken?: string; refreshToken?: string },
headers?: Record<string, string>,
) => {
if (provider?.setSession) {
provider.setSession(tokens, headers);
} else if (tokens?.accessToken && provider?.setTokens) {
provider.setTokens(tokens as { accessToken: string; refreshToken?: string });
if (headers && provider?.setCustomHeaders) {
provider.setCustomHeaders(headers);
}
}
setStatus("authenticated");
},
[provider],
);
const clearSession = useCallback(async () => {
provider?.clearTokens?.();
// Clear React Query cache
await queryClient.cancelQueries();
queryClient.clear();
setStatus("unauthenticated");
if (typeof window !== "undefined") {
window.dispatchEvent(new Event("auth:logout"));
const route = apiConfig.auth?.loginRoute || window.origin;
window.location.href = route;
}
}, [provider, queryClient]);
return {
status,
isAuthenticated: status === "authenticated",
isLoading: status === "loading",
getToken,
setSession,
clearSession,
};
};
useNotification.ts: Utility hook for useNotification.
import type { ReactNode } from "react";
import React, {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
export type NotificationType = "success" | "error" | "warning" | "info";
export interface ApiNotification {
id: string;
type: NotificationType;
message: string;
statusCode?: number;
timestamp: string;
}
export interface UseNotificationOptions {
maxNotifications?: number;
autoDismissMs?: number | false;
}
export const NOTIFICATION_EVENT = "api:notification" as const;
const DEFAULT_MAX = 10;
const DEFAULT_AUTO_DISMISS_MS = 5000;
interface NotificationContextValue {
notifications: ApiNotification[];
dismiss: (id: string) => void;
dismissAll: () => void;
pushNotification: (
notification: Omit<ApiNotification, "id" | "timestamp">,
) => void;
}
const NotificationContext = createContext<NotificationContextValue | null>(
null,
);
// Suppresses duplicate toasts when the same error fires multiple times in quick succession.
const DEDUPE_WINDOW_MS = 2500;
const recentNotifications = new Map<string, ReturnType<typeof setTimeout>>();
/**
* Pushes an error or status notification outside of React (e.g. from API service files).
* Identical notifications within 2.5 s are suppressed to prevent duplicate toasts.
*
* @example
* pushNotification({ type: "error", message: "Failed to load profile", statusCode: 500 });
*/
export const pushNotification = (
notification: Omit<ApiNotification, "id" | "timestamp">,
) => {
if (typeof window === "undefined") return;
const dedupeKey = `${notification.type}:${notification.statusCode ?? ""}:${notification.message}`;
if (recentNotifications.has(dedupeKey)) return;
// Auto-expire the key so the same error can reappear after the window closes.
const timer = setTimeout(() => recentNotifications.delete(dedupeKey), DEDUPE_WINDOW_MS);
recentNotifications.set(dedupeKey, timer);
window.dispatchEvent(
new CustomEvent(NOTIFICATION_EVENT, {
detail: {
...notification,
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
},
}),
);
};
export const NotificationProvider = ({
children,
options,
}: {
children: ReactNode;
options?: UseNotificationOptions;
}) => {
const {
maxNotifications = DEFAULT_MAX,
autoDismissMs = DEFAULT_AUTO_DISMISS_MS,
} = options ?? {};
const [notifications, setNotifications] = useState<ApiNotification[]>([]);
const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(
new Map(),
);
const dismiss = useCallback((id: string) => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
const timer = timersRef.current.get(id);
if (timer) {
clearTimeout(timer);
timersRef.current.delete(id);
}
}, []);
const dismissAll = useCallback(() => {
setNotifications([]);
timersRef.current.forEach((timer) => clearTimeout(timer));
timersRef.current.clear();
}, []);
useEffect(() => {
if (typeof window === "undefined") return;
// Captured for safe cleanup in React Strict Mode
const timers = timersRef.current;
const handleNotification = (event: Event) => {
const notification = (event as CustomEvent<ApiNotification>).detail;
if (!notification?.id) return;
setNotifications((prev) => {
if (prev.some((n) => n.id === notification.id)) return prev;
// Drop duplicates already on screen (guards against same-render-batch bursts).
if (prev.some((n) => n.message === notification.message && n.type === notification.type)) return prev;
const next = [...prev, notification];
return next.length > maxNotifications
? next.slice(next.length - maxNotifications)
: next;
});
if (autoDismissMs !== false) {
const timer = setTimeout(() => {
setNotifications((prev) =>
prev.filter((n) => n.id !== notification.id),
);
timers.delete(notification.id);
}, autoDismissMs);
timers.set(notification.id, timer);
}
};
window.addEventListener(NOTIFICATION_EVENT, handleNotification);
return () => {
window.removeEventListener(NOTIFICATION_EVENT, handleNotification);
timers.forEach((timer) => clearTimeout(timer));
timers.clear();
};
}, [maxNotifications, autoDismissMs]);
const contextValue: NotificationContextValue = {
notifications,
dismiss,
dismissAll,
pushNotification, // Reference the standalone function for ease of use from the hook
};
return React.createElement(
NotificationContext.Provider,
{ value: contextValue },
children,
);
};
/**
* Hook to consume the centralized notification state.
* Must be used within a <NotificationProvider>.
*/
export const useNotification = () => {
const context = useContext(NotificationContext);
if (!context) {
throw new Error(
"useNotification must be used within a NotificationProvider",
);
}
return context;
};useHeaders.ts: Utility hook for custom header management.
import { useCallback } from "react";
import { getActiveProvider } from "../auth-methods/manager";
/**
* Hook to manage custom API headers securely.
*/
export const useHeaders = () => {
const provider = getActiveProvider();
const getCustomHeaders = useCallback(
() => provider?.getCustomHeaders?.() ?? {},
[provider],
);
const setCustomHeaders = useCallback(
(headers: Record<string, string>) => {
provider?.setCustomHeaders?.(headers);
},
[provider],
);
const removeCustomHeader = useCallback(
(key: string) => {
provider?.removeCustomHeader?.(key);
},
[provider],
);
return { getCustomHeaders, setCustomHeaders, removeCustomHeader };
};providers/ - Query Provider
View code
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
export function QueryProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
return {children};
}types/ - TypeScript Interfaces
View Authentication Types
export type post_authLogin = unknown;export type post_authLogout = unknown;export type post_authRefresh = unknown;export type post_authSignup = unknown;View Todo Types
export type get_todos = unknown;export type post_todos = unknown;api.config.ts - User Configuration
This file is designed specifically for you to edit. It contains constants and mappings customized to your backend settings, merging both core API and authentication settings.
api.config.ts: Centralized configuration file.
View code
// @user-config — Update routes, keys, and endpoints to match your backend.
import axios from "axios";
import { type ReexConfig } from "./.reex/config";
// Replace this with your API base URL or an environment variable
// (e.g. process.env.NEXT_PUBLIC_API_URL).
const BASE_URL = "https://example.com/api/v1";
export const apiConfig: ReexConfig = {
baseURL: BASE_URL,
// Set to `true` if your API wraps every response in a top-level `data` property.
// When enabled, also turn on "Unwrap Response Data" in the Reex UI before
// saving or copying generated response interfaces.
unwrapResponseData: false,
// Enable request/response logging.
// For development only, consider:
// process.env.NODE_ENV === "development"
// or
// import.meta.env.DEV
enableApiLogging: false,
auth: {
// ---------------------------------------------------------------------------
// General authentication configuration
// ---------------------------------------------------------------------------
// The route users will be redirected to when logged out.
// If left undefined, it defaults to the application root (window.origin).
loginRoute: "/login",
// Refresh token endpoint. Update this to match your backend.
refreshEndpoint: `${BASE_URL}/auth/refresh`,
// ---------------------------------------------------------------------------
// JWT / Bearer token authentication
// Use these options when the `ReexProvider` auth strategy is `"jwt"`.
// ---------------------------------------------------------------------------
// Send the refresh token to your backend to obtain a new access token.
// Rename `refreshToken` in the request body if your backend expects
// a different field name (e.g. `refresh_token`).
refreshWithToken: async (storedRefreshToken: string) =>
axios.post(apiConfig.auth.refreshEndpoint, {
refreshToken: storedRefreshToken,
}),
// Extract authentication tokens from your backend response.
//
// Note: `responseBody` is affected by the `unwrapResponseData` setting.
// - When `unwrapResponseData` is `true`, `responseBody` contains the
// unwrapped `data` object.
// - When `unwrapResponseData` is `false`, `responseBody` contains the
// full response body.
//
// Update these paths to match your API response structure.
extractTokens: (responseBody: Record<string, unknown>) => ({
accessToken: responseBody?.accessToken as string | undefined,
refreshToken: responseBody?.refreshToken as string | undefined,
}),
// Storage key names for authentication tokens.
// Change these if you prefer different key names.
//
// Note: The access token is stored in memory by default.
// It is only persisted to localStorage if your backend does not
// return a refresh token.
accessTokenKey: "access_token",
refreshTokenKey: "refresh_token",
// ---------------------------------------------------------------------------
// Cookie-based authentication
// Use this when the `ReexProvider` auth strategy is `"cookie"`.
// ---------------------------------------------------------------------------
// Request a new access token using HttpOnly cookies.
refreshWithCookie: async () =>
axios.post(apiConfig.auth.refreshEndpoint, undefined, {
withCredentials: true,
// Add any headers required by your backend when refreshing tokens.
// headers: { "X-Client-ID": "1234" },
}),
},
};