-
Notifications
You must be signed in to change notification settings - Fork 0
Framework Best Practices
Using expo-router allows you to manage navigation with built-in abstractions instead of manually handling screen state. This follows React Native conventions, supports nested screens, back navigation, and deep linking, and avoids reinventing routing logic. Components used in expo router are the following: Stack, useRouter, Tabs, usePathname, Link, useFocusEffect, useLocalSearchParams, type {Href}
in frontend/app/(tabs)/_layout.tsx
import { Tabs, usePathname, useRouter } from "expo-router"; const pathname = usePathname();
const router = useRouter();
const { role, currentUser, isAuthLoading } = useAuth();
const currentTab = tabNavLinks.find(
(t) =>
`/${t.name}` === pathname || (t.name === "index" && pathname === "/"),
);
const requiresAuth = currentTab?.authRequired;
const requiresAdmin = currentTab?.adminOnly; useEffect(() => {
if (isAuthLoading) return;
if (requiresAuth && !currentUser) {
router.replace("/login");
return;
}
if (requiresAdmin && role !== "admin") {
router.replace("/dashboard");
return;
}
}, [
isAuthLoading,
currentUser,
role,
pathname,
router,
requiresAuth,
requiresAdmin,
]);<Tabs
screenOptions={{
headerShown: false,
tabBarButton: HapticTab,
}}
>
{/* Map over the tabNavLinks array */}
{tabNavLinks.map((tab) => (
<Tabs.Screen
key={tab.name}
name={tab.name}
options={
// Check if the tab should be hidden
tab.isHidden
? { href: null } // Hide the tab by setting href to null
: {
// Else show the tab with its title and icon
title: tab.title,
tabBarIcon: () => (
<Icon className="" size={24} as={tab.icon} />
),
}
}
/>
))}
</Tabs>By using @/components/ui, we're taking advantage of a consistent, reusable set of prebuilt interface components across the application. This abstraction makes UI development easier since it provides the benefit of consistency in style, it avoids duplicated code, and it ensures visual consistency. The components utilize modern React Native design best practices and make developing interfaces faster. The components also prevent developers from having to build a UI component from scratch every time.
in frontend/app/(tabs)/dashboard.tsx
import { Badge, Button, Icon, Text } from "@/components/ui";<View className="flex-row justify-between items-center mb-2">
<Text variant="h4">{item.airline_name}</Text>
<Badge variant={getStatusVariant(item.status)} label={item.status} />
</View>
<View className="flex-row justify-between items-center mb-3">
<Text variant="muted">
{item.flight_code} | {item.departure_airport_code}
<Icon
as={MoveRight}
size={14}
className="inline mx-1 pb-1 text-muted-foreground"
/>
{item.arrival_airport_code}
</Text>
<Badge variant="secondary" label={item.reason} />
</View><Button
onPress={() => router.push("/manual-claim")}
className="px-6 h-12 rounded-xl shadow-sm"
>
<Text className="text-primary-foreground font-semibold text-base">
Start your first claim
</Text>
</Button>Using useWindowDimensions allows the UI to adapt dynamically to different screen sizes on web and mobile platforms. This avoids hardcoding dimensions, follows React Native best practices, and ensures consistent layouts across devices.
in frontend/hooks/use-breakpoint.ts
import { useWindowDimensions } from "react-native";// Define all breakpoints
const TABLET_MIN_WIDTH = 768;
const DESKTOP_MIN_WIDTH = 1024;
export function useBreakpoint() {
const { width } = useWindowDimensions();
const [breakpoint, setBreakpoint] = useState({
isMobile: false,
isIos: false,
isAndroid: false,
isWeb: true,
isMobileWeb: false,
isTablet: false,
isDesktop: true, // Default to desktop view
});const isMobileWeb = isWeb && width < TABLET_MIN_WIDTH;
const isTablet =
isWeb && width >= TABLET_MIN_WIDTH && width < DESKTOP_MIN_WIDTH;
const isDesktop = isWeb && width >= DESKTOP_MIN_WIDTH;
setBreakpoint({
isMobile,
isIos,
isAndroid,
isWeb,
isMobileWeb,
isTablet,
isDesktop,
});
}, [width]);Using Hono allows us to compose the API using a modular structure. We use a main instance to handle global concerns (CORS, Rate Limiting, Logging) and delegate specific logic to sub-apps via api.route(). This ensures that as the project grows, the index.ts remains a high-level overview.
From backend/src/index.ts:
// We define a Type-Safe Context for User Sessions
const app = new Hono<{
Variables: {
user: typeof auth.$Infer.Session.user | null;
session: typeof auth.$Infer.Session.session | null;
};
}>();
// We compose the API using sub-routers
const api = new Hono();
api.route("/metrics", metricsApp);
api.route("/news", newsApp);
// We nest the entire API under a versioned prefix
app.route("/api", api);Instead of using try-catch blocks in every single route, we use a Global Error Handler (app.onError). This specifically catches Zod validation errors and HTTPExceptions, ensuring the frontend always receives a consistent JSON error structure (status code, error message, and validation details) regardless of which route failed.
From backend/src/index.ts:
app.onError((err, _c) => {
if (err instanceof HTTPException) {
const cause = err.cause;
// Specific handling for Zod validation errors
if (cause instanceof ZodError) {
return jsonPretty({
error: "Validation failed",
details: cause.issues.map((i) => ({ path: i.path.join("."), message: i.message })),
}, 400);
}
}
// Standardized Internal Server Error
return jsonPretty({ error: "Internal server error" }, 500);
});We leverage Hono’s middleware pattern to apply different behaviors based on the deployment environment. For example, we use structured JSON logging in production for better observability in log aggregators, while keeping human-readable strings in development for a better developer experience.
From backend/src/index.ts:
if (process.env.NODE_ENV === "production") {
// Structured JSON logging for cloud monitoring
app.use("*", async (c, next) => {
await next();
logInfo("HTTP Request", { method: c.req.method, path: c.req.path, status: c.res.status });
});
} else {
// Developer-friendly console logging
app.use("*", logger((str) => logInfo(str)));
}We use Drizzle ORM to maintain a "single source of truth" for our database. By defining the schema in TypeScript, we gain auto-completion and compile-time error checking. We use a Singleton Pattern for the database client to prevent exhausting connection pools in a serverless or long-running environment.
From backend/src/db/index.ts:
// We create a singleton pattern to reuse database connections
const globalForDb = globalThis as unknown as {
client?: ReturnType<typeof postgres>;
db?: ReturnType<typeof drizzle<typeof schema>>;
};
export const client = globalForDb.client ?? postgres(databaseUrl, { ssl: false });
if (process.env.NODE_ENV !== "production") globalForDb.client = client;
// We export the DB instance with the schema attached for relational queries
export const db = drizzle(client, { schema });
// We dedicate a client for migrations (restricted to 1 connection)
export const migrationDb = drizzle(postgres(databaseUrl, { max: 1 }), { schema });Instead of manually handling JWT tokens, we use the Better-Auth library. This centralizes all authentication logic, including email password resets and mobile deep-linking via the Expo plugin. By using the drizzleAdapter, Better-Auth automatically manages user, session, and account tables within our existing database.
From backend/src/lib/auth.ts:
export const auth = betterAuth({
// We integrate our database by syncing the auth tables with Drizzle
database: drizzleAdapter(db, {
provider: "pg",
}),
// The Expo plugin handles deep linking and mobile sessions
plugins: [
openAPI(),
expo(),
],
// Advanced cookie attributes for cross-domain production environments
advanced: {
defaultCookieAttributes: {
sameSite: "none",
secure: true,
partitioned: process.env.NODE_ENV === "production",
},
},
// We implement custom logic to hook into auth events like password resets
emailAndPassword: {
enabled: true,
async sendResetPassword({ user, url }) {
await sendEmail("Reset Your Password", <ResetPasswordTemplate url={url} />, { to: [user.email] });
},
},
});N8N's main appeal are its "nodes", which are highly configurable blocks of pre-written code, primarily used to facilitate the process of interacting with certain services or APIs, such as Google's Tesseract. However, as there are an infinite number of scenarios possible, there will be cases where using the "Code Node" block and writing your own code, tailored to the scenario, is better than chaining multiple N8N nodes together.
From n8n/workflows/flight-delays/Airline Claim Response Evaluator And Generator.json
let raw = $input.first().json.text || $input.first().json.output?.[0]?.content?.[0]?.text;
raw = raw
.replace(/```json/g, '')
.replace(/```/g, '')
.trim();
let parsed;
try {
parsed = JSON.parse(raw);
} catch (err) {
// If parsing fails, try a second cleanup pass (remove literal '\n')
const cleaned = raw.replace(/\\n/g, '\n').replace(/\\"/g, '"');
try {
parsed = JSON.parse(cleaned);
} catch (e2) {
return [{ json: { error: 'Failed to parse JSON', raw, cleaned } }];
}
}
return [{ json: parsed }];This is the process of creating an architecture that facilitates and heightens an AI Agent by providing it tools to aid it in its task. An example of this case is to provide an Agent with memory, as in some cases, such as with the chatbot, stateful conversations are necessary. Furthermore, we are providing the chatbot a tool, in our case SerpApi, to allow the model to more easily look up information. This is an example of implementing a RAG (Retrieval-Augmented Generation) principle.
From n8n/workflows/chatbot/ChatbotResponseGenerator.json
While LLM models such as Gemini are capable of extracting information from images, it is often preferable to follow a hybrid approach of combining both machine learning and deep learning together, optimized to using the strengths of each. For OCR, we first utilize Google's Tesseract, which is optimized for extracting text from images. As the potential results of the images may yield a wide variety of information, we then use Gemini to only retrieve what is relevant, which is then provided in a JSON.
From n8n/workflows/flight-delays/ocr.json