Skip to content
This repository was archived by the owner on May 9, 2026. It is now read-only.

Framework Best Practices

Tanzir Hoque edited this page Apr 13, 2026 · 25 revisions

Frontend Best Practices

1. Framework: Routing

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}

Code Implementation

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>

2. Framework: UI Components

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.

Code Implementation

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>

3. Framework: Responsive Layouts (useWindowDimensions)

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.

Code Implementation

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]);

Backend Best Practices

1. Framework: Centralized Routing & Middleware

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.

Code Implementation

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);

2. Framework: Global Error & Validation Handling

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.

Code Implementation

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);
});

3. Framework: Environment-Aware Middleware

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.

Code Implementation

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)));
}

4. Framework: Type-Safe Schema & Migrations

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.

Code Implementation

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 });

5. Framework: Session Management

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.

Code Implementation

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 Best Practices

1. Framework: Using Code Nodes

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.

Code Implementation

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 }];

2. Framework: AI-Agentic Workflow Design

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.

Code Implementation

From n8n/workflows/chatbot/ChatbotResponseGenerator.json

image

3. Framework: Hybrid Extraction/Retrieval

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.

Code Implementation

From n8n/workflows/flight-delays/ocr.json

image

Clone this wiki locally