-
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";
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";
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";
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] });
},
},
});