Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/elysia-eden-addon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/create': minor
---

Add a React Elysia and Eden Treaty integration with a same-origin typed API route and greeting demo.
80 changes: 80 additions & 0 deletions packages/cli/tests-e2e/elysia-smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { expect, test } from '@playwright/test'
import {
attachRuntimeGuards,
createReactAppFixture,
optimizePageForFastE2E,
} from './helpers'

for (const existing of [false, true]) {
test(`@blocking uses Elysia and Eden in a ${existing ? 'existing blank' : 'standard'} React app`, async ({
page,
}) => {
const fixture = await createReactAppFixture({
appName: `elysia-${existing ? 'existing' : 'standard'}`,
blank: existing,
packageManager: existing ? 'pnpm' : 'npm',
...(existing ? { postCreateAddOns: ['elysia'] } : { addOns: ['elysia'] }),
runQualityGatesChecks: true,
afterCreate: existing
? undefined
: async (appDir) => {
await writeFile(
join(appDir, 'src/elysia-contract.test-types.ts'),
`import { treaty } from '@elysiajs/eden'
import type { App } from './lib/elysia.server'
export function verifyTypes() {
const client = treaty<App>('http://unused.invalid')
// @ts-expect-error The API requires a string name.
void client.api.demo.elysia.greet.post({ name: 42 })
}
`,
)
},
})
const guards = attachRuntimeGuards(page, fixture.url)
try {
if (existing) {
const response = await page.request.post(
`${fixture.url}/api/demo/elysia/greet`,
{ data: { name: 'TanStack' } },
)
expect(response.status()).toBe(200)
expect(await response.json()).toEqual({ message: 'Hello TanStack' })
expect(
(await page.request.get(`${fixture.url}/demo/elysia`)).status(),
).toBe(404)
} else {
await optimizePageForFastE2E(page)
await page.emulateMedia({ colorScheme: 'dark' })
await page.goto(`${fixture.url}/demo/elysia`)
await expect(
page.getByRole('heading', { name: 'Elysia greeting' }),
).toBeVisible()
await page.getByLabel('Name', { exact: true }).fill('TanStack')
await page
.getByRole('button', { name: 'Greet', exact: true })
.click({ timeout: 10_000 })
await expect(page.getByRole('status')).toHaveText('Hello TanStack')
await test.info().attach('elysia-demo', {
body: await page.screenshot(),
contentType: 'image/png',
})
}
const invalid = await page.request.post(
`${fixture.url}/api/demo/elysia/greet`,
{ data: { name: 42 } },
)
expect(invalid.status()).toBe(422)
} finally {
try {
guards.assertClean()
} finally {
guards.dispose()
await fixture.stop()
await fixture.cleanup()
}
}
})
}
13 changes: 13 additions & 0 deletions packages/create/src/frameworks/react/add-ons/elysia/README.md.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Elysia + Eden

<% if (includeExamples && !blank) { %>Visit `/demo/elysia` after starting the app to send a typed greeting request to your Start server.<% } else { %>Send JSON such as `{"name":"TanStack"}` with a POST request to `/api/demo/elysia/greet` after starting the app. Blank projects omit the demo page.<% } %>

- Edit `src/lib/elysia.server.ts` to add Elysia endpoints and request schemas.
- `src/routes/api.demo.elysia.$.ts` forwards requests to the Elysia app.
- Use a type-only import of `App` when building an Eden Treaty client. Browser requests should use the current origin to support development and preview ports.

Elysia runs inside Start server routes. Start owns the server listener. Keep backend imports in server handlers and use type-only imports in client code.

The demo validates a name of 1–80 characters. Add authentication and authorization before exposing private data or privileged operations.

See the [Elysia Start integration guide](https://elysiajs.com/integrations/tanstack-start) for more examples.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Elysia, t } from 'elysia'

export const app = new Elysia({ prefix: '/api/demo/elysia' }).post(
'/greet',
({ body }) => ({ message: `Hello ${body.name}` }),
{ body: t.Object({ name: t.String({ minLength: 1, maxLength: 80 }) }) },
)

export type App = typeof app
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import { app } from '#/lib/elysia.server'

export const Route = createFileRoute('/api/demo/elysia/$')({
server: {
handlers: {
POST: ({ request }) => app.fetch(request),
},
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useEffect, useId, useState } from 'react'
import type { FormEvent } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { treaty } from '@elysiajs/eden'
import type { App } from '#/lib/elysia.server'
import '#/styles/elysia.css'

export const Route = createFileRoute('/demo/elysia')({ component: ElysiaDemo })

function ElysiaDemo() {
const nameId = useId()
const [ready, setReady] = useState(false)
const [message, setMessage] = useState('')
const [error, setError] = useState('')
const [pending, setPending] = useState(false)

useEffect(() => setReady(true), [])

async function greet(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
if (pending) return
const name = new FormData(event.currentTarget).get('name')
if (typeof name !== 'string' || !name.trim()) {
setError('Enter a name.')
return
}
setPending(true)
setMessage('')
setError('')
try {
const client = treaty<App>(window.location.origin)
const response = await client.api.demo.elysia.greet.post({
name: name.trim(),
})
if (response.error) {
setError('Could not get a greeting. Try again.')
} else {
setMessage(response.data.message)
}
} catch {
setError('Could not get a greeting. Check your connection and try again.')
} finally {
setPending(false)
}
}

return (
<main className="elysia-demo">
<h1>Elysia greeting</h1>
<form onSubmit={(event) => void greet(event)}>
<label htmlFor={nameId}>Name</label>
<input
id={nameId}
name="name"
required
maxLength={80}
autoComplete="given-name"
/>
<button type="submit" disabled={pending || !ready}>
{pending ? 'Greeting…' : 'Greet'}
</button>
</form>
{message && <output aria-live="polite">{message}</output>}
{error && <p role="alert">{error}</p>}
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.elysia-demo {
max-width: 36rem;
margin: 3rem auto;
padding: 1.5rem;
color: var(--sea-ink, #1f2937);
}
.elysia-demo h1 {
font-size: 1.75rem;
margin: 0 0 1.5rem;
}
.elysia-demo form {
display: grid;
gap: 0.75rem;
}
.elysia-demo input {
padding: 0.65rem;
border: 1px solid var(--line, #94a3b8);
border-radius: 0.4rem;
background: var(--surface, #fff);
color: inherit;
font: inherit;
}
.elysia-demo button {
justify-self: start;
padding: 0.65rem 1rem;
border: 1px solid var(--line, #94a3b8);
border-radius: 0.4rem;
background: color-mix(
in oklab,
var(--lagoon, #4fb8b2) 22%,
var(--surface-strong, #fff)
);
color: var(--sea-ink, #173a40);
font: inherit;
cursor: pointer;
}
.elysia-demo button:disabled {
opacity: 0.6;
cursor: default;
}
.elysia-demo output {
display: block;
margin-top: 1.5rem;
}
.elysia-demo [role='alert'] {
margin-top: 1.5rem;
}
21 changes: 21 additions & 0 deletions packages/create/src/frameworks/react/add-ons/elysia/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "Elysia + Eden",
"description": "Typed same-origin API routes with Elysia and Eden Treaty.",
"phase": "add-on",
"type": "add-on",
"category": "api",
"modes": [
"file-router"
],
"link": "https://elysiajs.com/integrations/tanstack-start",
"color": "#A78BFA",
"routes": [
{
"icon": "Network",
"url": "/demo/elysia",
"name": "Elysia",
"path": "src/routes/demo/elysia.tsx",
"jsName": "ElysiaDemo"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"dependencies": {
"elysia": "^1.4.31",
"@elysiajs/eden": "^1.4.9",
"@sinclair/typebox": "^0.34.41",
"openapi-types": "^12.1.3",
"file-type": "^21.3.2"
}
}
Loading