File-Based Routing: Your Folder Structure Is Your Navigation
TL;DR File-based routing = the file path is the route. No routes.js, no navigator registration. Expo Router: every file in app/ is a route. Next.js App Router: only page.tsx makes a route; folders are the segments.
TL;DR
-
File-based routing = the file path is the route. No
routes.js, no navigator registration. -
Expo Router: every file in
app/is a route. Next.js App Router: onlypage.tsxmakes a route; folders are the segments. - Six conventions cover ~everything:
index,[param],[...catchAll],_layout,(group),+not-found. - Biggest trap: renaming a file changes its URL and silently breaks deep links. Treat it like an API change.
- Auth guards still need code, usually a
<Redirect />in a layout.
I used to have a ~300-line AppNavigator.tsx in every project that nobody wanted to touch. Adding a screen meant: create the component → register it → type its params. Skip step 2, crash. Skip step 3, any everywhere.
File-based routing deletes that file. Here's how it works, mostly in code.
Files → routes
Expo Router: every file is a route:
app/index.tsx → /
app/about.tsx → /about
app/blog/[slug].tsx → /blog/any-post-name
app/settings/profile.tsx → /settings/profile
Next.js App Router: folders are segments, page.tsx is the route:
app/page.tsx → /
app/about/page.tsx → /about
app/blog/[slug]/page.tsx → /blog/any-post-name
app/settings/profile/page.tsx → /settings/profile
Same idea, different rule for what counts as a route. Remember that, because it matters later.
The 6 conventions
1. Index routes
app/blog/index.tsx (Expo) or app/blog/page.tsx (Next) → /blog.
2. Dynamic segments
Expo, via a hook:
// app/user/[id].tsx
import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
export default function User() {
const { id } = useLocalSearchParams<{ id: string }>();
return <Text>User {id}</Text>;
}
Next.js 15+, via props, where params is now a Promise:
// app/user/[id]/page.tsx
export default async function User({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <h1>User {id}</h1>;
}
3. Catch-all
// app/docs/[...slug].tsx → /docs/a/b/c
const { slug } = useLocalSearchParams<{ slug: string[] }>();
// slug === ['a', 'b', 'c']
4. Layouts
A layout wraps every route in its folder and stays mounted. In React Native, this is where you pick the navigator:
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import Ionicons from '@expo/vector-icons/Ionicons';
export default function TabsLayout() {
return (
<Tabs>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <Ionicons name="home" size={22} color={color} />,
}}
/>
<Tabs.Screen name="search" options={{ title: 'Search' }} />
<Tabs.Screen name="profile" options={{ title: 'Profile' }} />
</Tabs>
);
}
Navigators are folders now. That's the whole mental shift.
5. Route groups
(parens) = organise files without adding a URL segment. app/(auth)/login.tsx → /login. Use groups to give screens different layouts (tabs vs. plain stack).
6. Not-found
+not-found.tsx (Expo) / not-found.tsx (Next). Add it on day one; a blank screen on a bad deep link looks like a crash.
A real structure
app/
├── _layout.tsx # root Stack
├── (auth)/
│ ├── _layout.tsx # plain Stack, no tabs
│ ├── login.tsx # /login
│ └── signup.tsx # /signup
├── (tabs)/
│ ├── _layout.tsx # Tabs
│ ├── index.tsx # /
│ ├── search.tsx # /search
│ └── profile.tsx # /profile
├── post/
│ └── [id].tsx # /post/42, pushed over tabs
└── +not-found.tsx
You can read the entire app's navigation from ls -R app/. Try that with a hand-rolled navigator.
Navigating
import { Link, router } from 'expo-router';
<Link href="/post/42">Open post</Link>
// with params object
<Link href={{ pathname: '/post/[id]', params: { id: '42' } }}>Open post</Link>
router.push('/settings/profile');
router.replace('/login'); // no back entry
router.back();
Turn on typed routes and typos become compile errors:
// app.json
{
"expo": {
"experiments": { "typedRoutes": true }
}
}
<Link href="/psot/42" />
// ^^^^^^^^ Type error: not a valid route
Where it bites
Renaming = breaking change
git mv app/settings/profile.tsx app/account/profile.tsx changes the URL. Push notification payloads, emails, bookmarks and SEO all still point at the old path. Leave a redirect behind:
// app/settings/profile.tsx (keep this file after the move)
import { Redirect } from 'expo-router';
export default function Moved() {
return <Redirect href="/account/profile" />;
}
In Next.js, use redirects() in next.config.js instead.
Non-route files in app/
-
Expo Router: everything in
app/is a route. DropButton.tsxin there and you've shipped a screen called/Button. Keep shared code incomponents/,lib/,hooks/. -
Next.js: only
page.tsx/route.tsetc. are routes, so colocating components is safe. Still, many teams keep them out for clarity.
Deep nesting
Three levels of groups, each with its own _layout.tsx, and nobody can tell which layout wraps which screen. If you need a diagram for your folder tree, flatten it.
Auth guards are still code
The folder tree can't express "signed-in users only." Do it in the layout:
// app/(tabs)/_layout.tsx
import { Redirect, Tabs } from 'expo-router';
import { useSession } from '@/lib/session';
export default function TabsLayout() {
const { user, isLoading } = useSession();
if (isLoading) return null;
if (!user) return <Redirect href="/login" />;
return <Tabs />;
}
Newer Expo Router versions also have Stack.Protected for declarative guards. In Next.js this usually goes in middleware.ts or a server-side check in layout.tsx.
Should you switch?
- New project: yes. Both Expo and Next.js default to it; fighting the default costs more than it saves.
- Existing React Navigation app: migrate one flow at a time (onboarding is easiest) and keep a redirect for every URL you move.
If you want to poke at a working tree before designing your own, RapidNative generates Expo apps with the app/ folder, layouts and groups already wired up, and you can read and edit them like normal code.
Wrap-up
Six conventions, keep non-route code out of app/, and treat every rename as a URL change. That's it.
How is your app/ folder organised? Drop your tree in the comments. I'm curious how people use route groups in production.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.