Sharing code between Next.js and Expo in a Turborepo monorepo
Published
Short answer
Put the apps in apps/ (Next.js for web, Expo for mobile) and shared code in packages/ (types, validation schemas, an API client, constants), keep UI components separate per platform, and let Turborepo run tasks in dependency order and cache them. Share logic, not components. At Qrispy I set up a Turborepo monorepo with shared packages for production web and mobile apps.
The folder layout
apps/
web/ # Next.js
mobile/ # Expo (React Native)
packages/
shared/ # types, zod schemas, API client, constants
config/ # shared tsconfig and eslint settings
turbo.json
package.jsonWhat to share and what not to
- Share: TypeScript types, zod validation schemas, the API client, business rules, constants and translation strings.
- Don't share: UI components and navigation. The web renders DOM and mobile renders React Native components, and forcing them together makes both worse.
That way web and mobile use the same data shapes and validation rules, while each platform is free to build its own interface.
Wire up the packages
Add the shared package as a dependency of each app. With pnpm, the workspace protocol is enough.
{
"dependencies": {
"@acme/shared": "workspace:*"
}
}If you share the package as TypeScript source instead of built output, tell Next.js with transpilePackages. Recent Expo versions configure Metro for monorepos automatically; on older ones you may need to set watchFolders in metro.config.js.
// apps/web/next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@acme/shared"],
};
export default nextConfig;turbo.json
Turborepo builds a package before the apps that use it and serves unchanged work from its cache.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {},
"dev": { "cache": false, "persistent": true }
}
}Things to watch
- React version: React Native is tied to a specific React version. Make sure only one React version ends up in the monorepo, or you get strange errors.
- Environment variables belong to each app. Don't put app secrets in the shared package.
- Keep TypeScript and eslint settings in packages/config and extend them from the apps.
Questions
- Can I share UI components between web and mobile?
- You can, but for most teams it isn't worth it. Sharing types, validation, the API client and business rules gives the biggest gain with the least friction.
- Are pnpm workspaces enough without Turborepo?
- Workspaces link the packages. Turborepo adds task ordering and caching on top, which cuts build time a lot once you have several apps and packages.
- Why won't Next.js compile my shared package?
- If you share the package as TypeScript source, add it to the transpilePackages list in next.config.
I do this kind of work
Written by Mert Yılmaz, freelance fullstack developer.