API Setup
1. Install Axios
To manage HTTP requests efficiently, we will use Axios, a widely-used promise-based HTTP client.
pnpm add axios
2. Install Bun
Bun is a fast JavaScript runtime that will be used for executing server-side code.
npm install -g bun
For detailed instructions, refer to the Bun Installation Guide.
3. Configure Environment Variables
In the root directory of your project, create a .env.local
file and define the following variables:
VITE_DATABASE = <DATABASE>VITE_FQ_BASE_URL = <BASE_URL>VITE_FQ_LOCAL_SERVER = http://localhost:4466VITE_FQ_TOKEN_PATH = <FULL_TOKEN_PATH> // e.g., /path/to/your/project/src/services/tokens.json
- Replace
<DATABASE>
with your database name. - Replace
<AUTH>
with your authentication credentials. - Replace
<BASE_URL>
with the base URL of your API server. - Replace
<FULL_TOKEN_PATH>
with the full path to yourtokens.json
file, which will be created later.
4. API Service Configuration
Create or update the src/services/Api.ts
file with the following content to set up a flexible API service:
import axios, { type AxiosRequestConfig } from "axios";import tokens from "./tokens.json";
interface Tokens { [key: string]: string | false;}
const DATABASE = import.meta.env.VITE_DATABASE;const FQ_BASE_URL = import.meta.env.VITE_FQ_BASE_URL;const FQ_LOCAL_SERVER = import.meta.env.VITE_FQ_LOCAL_SERVER;const FQ_TOKEN_PATH = import.meta.env.VITE_FQ_TOKEN_PATH;
// Base64 encoding charactersconst base64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function toBase64(num: number): string { let result = ""; const str = num.toString(); for (let i = 0; i < str.length; i++) { const charCode = parseInt(str[i]); result += base64chars[charCode % 64]; } return result;}
type HttpMethod = "get" | "post" | "put" | "delete" | "sql";
type SQLBody = { sql: "string"; params: [{ [key: string]: string | number }];};type AnyBody = { [key: string]: any;};
type RequestOptions = { loading?: boolean; body?: SQLBody | SQLBody[] | AnyBody | AnyBody[]; key?: string; page?: string; sort?: string; joins?: string; filter?: string; search?: string; nearby?: string; hidden?: string; fields?: string; session?: string; validation?: string; permissions?: string;};
function uniqueKey(input: string) { let code = input.charCodeAt(0); for (let i = 0; i < input.length; i++) { const char = input.charCodeAt(i); code = (code << 5) - code + char; code &= code; }
return toBase64(Math.abs(code)).substring(0, 8);}
function getKey(method: HttpMethod, url: string, options: RequestOptions) { if (!FQ_LOCAL_SERVER) { throw new Error("localServer is not defined"); }
const _url = FQ_LOCAL_SERVER + url; const parsed_url = new URL(_url); const pathname = "/" + parsed_url.pathname.split("/")[1];
const request: any = { fields: options?.fields, hidden: options?.hidden, filter: options?.filter, nearby: options?.nearby, collections: options?.joins, permissions: options?.permissions, validation: options?.validation, };
request["body_is_array"] = Array.isArray(options.body || {});
let tokenStr = pathname; for (const key in request) { if (request[key]) { tokenStr += key + ":" + request[key]; } } const key = method + ":" + pathname + ">" + uniqueKey(tokenStr); return key;}
const makeRequest = async (method: HttpMethod, endpoint: string, options: RequestOptions = {}): Promise<unknown> => { const { body, page, sort, joins, hidden, fields, filter, search, nearby, session, validation, permissions, loading = true, } = options;
const headers: any = {};
if (hidden) headers.hidden = hidden; if (filter) headers.filter = filter; if (fields) headers.fields = fields; if (session) headers.session = session; if (nearby) headers.nearby = nearby; if (joins) headers.collections = joins; if (validation) headers.validation = validation; if (permissions) headers.permissions = permissions;
const key = getKey(method, endpoint, options); const token = (tokens as Tokens)[key] || false;
if (!token) { headers["key"] = key; headers["token-path"] = FQ_TOKEN_PATH; } else { headers.token = token; }
const params: { [key: string]: string | number | boolean | object | undefined } = { page: page, sort: sort, search: search, };
try { if (loading) { console.log("Loading started..."); }
const axiosInstance = axios.create({ baseURL: token ? FQ_BASE_URL : FQ_LOCAL_SERVER, headers: { app: DATABASE }, });
const requestConfig: AxiosRequestConfig = { method, params, headers, data: body, url: endpoint, };
const response = await axiosInstance(requestConfig); return response.data; } catch (error: any) { console.error(`${method.toUpperCase()} Error:`, error.message); throw error; } finally { if (loading) { console.log("Loading completed."); } }};
const Api = { get: async (endpoint: string, options?: RequestOptions): Promise<any> => makeRequest("get", endpoint, options), put: async (endpoint: string, options?: RequestOptions): Promise<any> => makeRequest("put", endpoint, options), post: async (endpoint: string, options?: RequestOptions): Promise<any> => makeRequest("post", endpoint, options), delete: async (endpoint: string, options?: RequestOptions): Promise<any> => makeRequest("delete", endpoint, options), sql: async (endpoint: string, options?: RequestOptions): Promise<any> => makeRequest("post", `/sql-${endpoint.replace("/", "")}`, options),};
export default Api;
5. Create an Empty Tokens File
Create an empty JSON file at src/services/tokens.json
:
{}
6. Set Up Local FrontQL Server
To run a local server that proxies requests to FrontQL, create a new file named server.js
in a directory outside your project (to avoid leaking sensitive information). This server will handle authentication and proxy requests to the FrontQL API.
// <your-preferred-directory>/server.jsimport { serve, file, write, fetch } from "bun";
const PORT = 4466;const AUTH_FILE = "./auth.json";const AUTH_URL = "https://auth.frontql.dev/login";const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000;
const HOSTNAME = "<YOUR_FRONTQL_HOSTNAME>";const FQ_USERNAME = "<YOUR_FRONTQL_USERNAME>";const FQ_PASSWORD = "<YOUR_FRONTQL_PASSWORD>";
const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*",};
/** * Attempt login and return `Basic` token string or error Response. */async function login(app, username, password) { try { const response = await fetch(AUTH_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password, app: app || "test" }), });
const data = await response.json(); if (!response.ok || data.err) { return new Response(JSON.stringify({ err: true, result: data.result || "Login failed" }), { status: 401, headers: { "Content-Type": "application/json", ...CORS_HEADERS }, }); }
return `Basic ${data.result}`; } catch (err) { return new Response(JSON.stringify({ err: true, result: "Auth server unreachable" }), { status: 500, headers: { "Content-Type": "application/json", ...CORS_HEADERS }, }); }}
/** * Return a valid token string from cache or login again if expired. */async function handleAuthentication(headers) { let authData = {}; try { authData = await file(AUTH_FILE).json(); } catch { return new Response(JSON.stringify({ err: true, result: "Authentication data not found" }), { status: 404, headers: { "Content-Type": "application/json", ...CORS_HEADERS }, }); }
const app = headers.get("app") || "test"; const entry = authData[app]; const isValid = entry && entry.username === FQ_USERNAME && Date.now() - entry.lastLoggedIn < TOKEN_EXPIRY_MS;
if (isValid) { return entry.token; }
const newToken = await login(app, FQ_USERNAME, FQ_PASSWORD); if (newToken instanceof Response) return newToken;
authData[app] = { lastLoggedIn: Date.now(), username: FQ_USERNAME, token: newToken, };
await write(AUTH_FILE, JSON.stringify(authData, null, 2)); return newToken;}
/** * Main proxy handler */serve({ port: PORT, async fetch(req) { if (req.method === "OPTIONS") { return new Response("OK", { headers: CORS_HEADERS }); }
const tokenOrResponse = await handleAuthentication(req.headers); if (tokenOrResponse instanceof Response) return tokenOrResponse; const authToken = tokenOrResponse;
const url = new URL(req.url); url.hostname = HOSTNAME; url.protocol = "https:"; url.port = "443";
const bodyText = await req.text(); const key = req.headers.get("key"); const tokenPath = req.headers.get("token-path");
const headers = new Headers(req.headers); headers.delete("host"); headers.delete("Authorization"); headers.set("Authorization", authToken); headers.set("Accept-Encoding", "br");
let responseBody; try { const response = await fetch(url, { method: req.method, headers, body: ["GET", "HEAD"].includes(req.method) ? undefined : bodyText, }); responseBody = await response.json(); } catch { return new Response(JSON.stringify({ err: true, result: "Target service unreachable" }), { status: 502, headers: { "Content-Type": "application/json", ...CORS_HEADERS }, }); }
if (key && responseBody?.token && tokenPath) { let tokens = {}; try { tokens = await file(tokenPath).json(); } catch {} tokens[key] = responseBody.token; await write(tokenPath, JSON.stringify(tokens, null, 2)); }
return new Response(JSON.stringify(responseBody), { status: 200, headers: { "Content-Type": "application/json", ...CORS_HEADERS, }, }); },});
console.log(`π frontql dev server running: http://localhost:${PORT}`);
7. Create a auth.json
File
Create an auth.json
file in the same directory as your server file. This file will store authentication data:
{}
8. Organize API Calls
To maintain a clean project structure, it is recommended to organize your API calls into separate modules. For example, for the βusersβ related API calls, create a src/apis/users.js
file:
import Api from "services/Api";
export async function getUsers() { const response = await Api.get("/users"); return response;}
9. Consolidate API Calls in a Single File
You can create an index.js
file in the src/apis
directory to export all your API modules. This allows you to import them easily in other parts of your application:
export * from "./users";export * from "./products";// Add other API modules as needed
10. Start the Local Server with Bun
To run your local server, open a terminal and execute the following command:
bun <your-server-directory-path>/server.js
Replace
<your-server-directory-path>
with the actual path to your server directory, which contains theserver.js
file.