Welcome to Aetherspace! Set up your Web, iOS & Android app in minutes, and blow away the competition with write-once components that run on all platforms. Powered by GraphQL, React, Expo & Next.js
Generate a new repo from the Aetherspace template and (optionally) include all branches.
Github will then generate a copy of the template repo for you to customize. It comes out of the box with setup for:
- Next.js web app (File based app dir routing, Serverside rendering, Static site generation, ...)
- Expo mobile app (Android & iOS with Expo-Router and react-native)
- A REST & GraphQL API (with Apollo Server & Next.js API routes)
- Generators and Automation scripts to automatically generate API & component documentation
- Documentation for Aetherspace and its components (with docgen being a side effect of our recommended way of working)
- Github actions for mobile deployments, linting your code & building your documentation
When you're ready to start developing, run yarn install
to install all dependencies, followed by:
yarn dev:docs
packages/@aetherspace
houses a bunch of helpers that can be imported under the aetherspace
namespace. These aim to make your cross-platform journey a breeze. All of it has been written in Typescript and is documented in Storybook and the repo README's. Feel free to edit these to your liking if you make changes, but here's what it you can do with it out of the box:
💚 Aetherspace primitives are built with tailwind, iOS, Android, web, node and ssr (+ media queries) in mind.
We mention media queries specifically because react-native-web does not support them out of the box. But we've got you covered.
import { View, Text } from 'aetherspace/primitives'
export const MyComponent = () => (
<View tw="px-2 max-w-[100px] items-center rounded-md">
<Text tw="lg:text-xl font-primary-bold text-green">
Hello World 👋
</Text>
</View>
)
✨ Thanks to a tiny wrapper around
@expo/html-elements
and primitives likeImage
using either the optimized Next.js or React-Native component under the hood, anything you build with these primitives will automatically be optimized for each platform as well 🎉
import { Image } from 'aetherspace/primitives'
import { Article, Section, H2, P } from 'aetherspace/html-elements'
// -i- Web: Renders article / section / h2 + next/image for better SEO & web vitals
// -i- Mobile: Renders react-native View / Text / ... 👉 Gets turned into actual native UI
export const MyBlogPost = (props: { paragraphs: string[] }) => (
<Article tw="relative">
<H2 tw="text-gray font-primary-black">My post title</H2>
<Image tw="w-full" src="/img/article-header.png">
<Section tw="px-4 mb-4">
{/* render each paragraph as a <p> tag on web, or <Text> on mobile */}
{props.paragraphs.map((paragraph) => <P tw="font-primary-regular">{paragraph}</P>)}
</Section>
</Article>
)
⏳ To start everything, but automatically wait for your Next.js server to start before starting up Expo & Storybook:
yarn dev:docs
This will run the dev
command in each app workspace in parallell with Turborepo 👇
## Starts next.js web project + API routes on port 3000
next-app:dev: $ next
next-app:dev: ready - started server on 0.0.0.0:3000, url: http://localhost:3000
## Runs automations like docgen at next.js build time
next-app:dev: -i- Successfully created resolver registry at:
next-app:dev: ✅ packages/@registries/resolvers.generated.ts
next-app:dev: -i- Auto documenting with 'yarn document-components' ...
next-app:dev: ✅ packages/@registries/docs/features/app-core/icons.stories.mdx
next-app:dev: ✅ packages/@registries/docs/features/app-core/screens.stories.mdx
## Checks whether backend is ready
aetherspace:dev-health-check: $ NODE_ENV=development node scripts/dev-health-check
aetherspace:dev-health-check: ✅ Health check 1 succeeded: Server, API routes & GraphQL up and running.
## Starts Expo for mobile dev in iOS / Android device or simulator
expo-app:start: $ npx expo start
expo-app:start: Your native app is running at exp://192.168.0.168:19000
## Starts up Storybook for developing & testing components in isolation
99% done plugins webpack-hot-middlewarewebpack built preview acfe5466784b8a1a2429 in 162ms
╭─────────────────────────────────────────────────────╮
│ │
│ Storybook 6.5.10 for React started │
│ 3.12 s for manager and 8.9 s for preview │
│ │
│ Local: http://localhost:6006/ │
│ On your network: http://169.254.34.142:6006/ │
│ │
╰─────────────────────────────────────────────────────╯
📐 Our
aetherSchema()
structure builder enables you to build for Typescript first (with Zod), but enables you to optionally generate documentation, validation logic, GraphQL typedefs and data resolvers from those schemas as well.
import { z, aetherSchema, AetherProps } from 'aetherspace/schemas'
/* --- Schematypes ------------- */
export const PropSchema = aetherSchema('MyComponentProps', {
name: z.string().optional(), // string | undefined
value: z.number().default(1), // number
})
/* --- <MyComponent/> ---------- */
// Infer types from props schema with 'AetherProps' type helper, or ...
export const MyComponent = (props: AetherProps<typeof PropSchema>) => {
// Prop validation + apply defaults
const { name, value } = PropSchema.parse(props)
// ...
}
📚 Documentation drives adoption... and Storybook is a great way to do it.
However, it can be a pain to set up and maintain the docs for every component manually. Luckily, we've already set it up for you. On top of that, all you need to do is assign youraetherSchema()
&zod
powered prop definition as agetDocumentationProps
export and our scripts will automatically turn it into Storybook controls.
../../components/MyComponent.tsx
const PropSchema = aetherSchema('MyComponentProps', {
name: z.string().optional().describe('Title of the component'),
value: z.number().default(1).describe('Initial value of the component'),
})
/* --- <MyComponent/> ---------- */
// ... export your component with the same name as the file ...
/* --- Documenation ------------ */
export const getDocumentationProps = PropSchema.introspect()
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
next-app:dev: -i- Auto documenting with 'yarn document-components' ...
next-app:dev: ✅ packages/@registries/docs/features/app-core/icons.stories.mdx
next-app:dev: ✅ packages/@registries/docs/features/app-core/screens.stories.mdx
💪 Using
aetherSchema()
&zod
to describe function arguments and responses opens it up to not just easier regular function use with async / await, but Next.js API routes and GraphQL resolvers as well.
features/app-core/routes/api/health/route.ts
// Schemas
import { z, aetherSchema } from 'aetherspace/schemas'
import { aetherResolver, makeNextApiHandler, makeGraphQLResolver } from 'aetherspace/utils/serverUtils'
/* --- Schemas ------------- */
export const HealthCheckArgs = aetherSchema('HealthCheckArgs', {
echo: z.string().optional().describe('Echoes back the echo argument'),
})
// (You can reuse schema definitions with pick / omit / extend commands as well)
export const HealthCheckResponse = HealthCheckArgs.pickSchema('HealthCheckResponse', {
echo: true, // <- Pick the echo argument from the args schema, since we're echoing it back
})
/* --- Config -------------- */
const resolverConfig = {
argsSchema: HealthCheckArgs,
responseSchema: HealthCheckResponse,
}
/* --- healthCheck() ------- */
// Our actual business logic
export const healthCheck = aetherResolver(async ({ args }) => ({
echo: args.echo, // <- Echo back the echo argument 🤷♂️
}), resolverConfig)
/* --- Next.js API Routes -- */
export const GET = makeNextRouteHandler(healthCheck)
export const POST = makeNextRouteHandler(healthCheck)
/* --- GraphQL ------------- */
// Make resolver available to GraphQL (picked up by automation)
export const graphResolver = makeGraphQLResolver(healthCheck)
example >>> REST (e.g. at /api/health
)
⚛️ Since we're exporting a
graphResolver
function usingmakeGraphQLResolver()
, we can generate a GraphQL endpoint for our resolver function as well:
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
next-app:dev: -i- Successfully created resolver registry at:
next-app:dev: ✅ packages/@registries/resolvers.generated.ts
/api/graphql
will then use the resolvers.generated.ts
barrel file to build its graphql API from.
example >>> GraphQL (e.g. in /api/graphql
)
Performing these 6 steps has provided us with a bunch of value in little time:
- Hybrid UI component that is styled with tailwind, but actually native on iOS and Android
- Hybrid UI component that is optimized for SEO, media queries and Web-Vitals on Web
- Storybook documentation without having to explicitly create it ourselves
- 🤝 A single source of truth for all our props, args, responses, docs, types, defaults and validation
- A Back-end resolver function we can call from other data resolvers or API routes
- A GraphQL API powered by Apollo-Server, with automatically inferred type definitions
- A Next.js powered REST API