Skip to content

API Setup

1. Install Axios

To manage HTTP requests efficiently, we will use Axios, a widely-used promise-based HTTP client.

Terminal window
pnpm add axios

2. Install Bun

Bun is a fast JavaScript runtime that will be used for executing server-side code.

Terminal window
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:

.env.local
VITE_DATABASE = <DATABASE>
VITE_FQ_BASE_URL = <BASE_URL>
VITE_FQ_LOCAL_SERVER = http://localhost:4466
VITE_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 your tokens.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:

src/services/Api.ts
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 characters
const 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.js
import { 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:

src/apis/users.js
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:

src/apis/index.js
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:

Terminal window
bun <your-server-directory-path>/server.js

Replace <your-server-directory-path> with the actual path to your server directory, which contains the server.js file.