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
9 changes: 2 additions & 7 deletions .cta.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,5 @@
"routerOnly": false,
"version": 1,
"framework": "react",
"chosenAddOns": [
"eslint",
"nitro",
"prisma",
"tanstack-query"
]
}
"chosenAddOns": ["eslint", "nitro", "prisma", "tanstack-query"]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ dist-ssr
.vinxi
__unconfig*
todos.json
**/src/generated/
4 changes: 3 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package-lock.json
pnpm-lock.yaml
yarn.lock
yarn.lock
.tanstack
**/src/generated/
83 changes: 74 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,77 @@

Our plan/spec as it is now, is outlined in [./docs/inital-plan.md](./docs/initial-plan.md)

## Monorepo structure

This project is a pnpm workspace monorepo:

```text
apps/
frontend/ TanStack Start app (React + Vite, port 3000)
backend/ Apollo Server + GraphQL API (Pothos + Prisma, port 4000)
packages/
db/ Prisma schema, generated client, migrations and seed
```

The backend builds its GraphQL schema with [Pothos](https://pothos-graphql.dev) and
the [Prisma plugin](https://pothos-graphql.dev/docs/plugins/prisma), which maps each
GraphQL selection set to a matching Prisma `select`/`include`. Queries only touch the
columns and relations the client actually asked for.

## Getting started

```bash
# Set up the database
# You can decide to run postgres as a service locally, or use the docker compose script provided to start a container
pnpm db:setup # Runs a script to set up a bare-metal postgres instance with the correct setup
# OR
docker compose up -d

# install dependencies for all workspaces
pnpm install

# set up the database connection in each package
cp packages/db/.env.example packages/db/.env
cp apps/backend/.env.example apps/backend/.env

# create the database schema and optionally seed it
pnpm db:generate
pnpm db:push
pnpm db:seed

# run the frontend and backend together with live reload
pnpm dev
```

Frontend: http://localhost:3000 · GraphQL API: http://localhost:4000

### Commands

| Command | What it does |
| ------------------------------------------ | ------------------------------------------------------ |
| `pnpm install` / `pnpm install:all` | Install dependencies in all workspaces |
| `pnpm dev` | Run frontend and backend concurrently with live reload |
| `pnpm build` | Build all workspaces that define a build |
| `pnpm typecheck` | Typecheck all workspaces |
| `pnpm lint` / `pnpm format` / `pnpm check` | Lint, auto-fix format, and check formatting |
| `pnpm db:setup` | Provision PostgreSQL natively on Ubuntu (see below) |
| `pnpm db:generate` | Regenerate the Prisma client and Pothos types |
| `pnpm db:push` | Push the Prisma schema to the database |
| `pnpm db:migrate` | Create/apply a development migration |
| `pnpm db:seed` | Seed the database |
| `pnpm db:studio` | Open Prisma Studio |
| `pnpm generate-routes` | Regenerate the TanStack Router route tree |

### Provisioning the database on the Ubuntu VM

`pnpm db:setup` runs [scripts/setup-db.sh](./scripts/setup-db.sh), which installs
PostgreSQL natively on Ubuntu (apt-based systems only), enables the service, and
creates the same user, password, and database as `docker-compose.yml`
(`gspot`/`gspotter`/`gspot` on port 5432). It is idempotent and safe to re-run.
On Ubuntu 24.04 it installs Postgres 16, matching the pinned compose image; on
22.04 it falls back to the distro default. Afterwards, set the `DATABASE_URL` it
prints in `packages/db/.env` and `apps/backend/.env`, then run `pnpm db:push`.

## Below is the default TanStack Start readme, it will be replaced

Welcome to your new TanStack Start app!
Expand Down Expand Up @@ -38,7 +109,6 @@ If you prefer not to use Tailwind CSS:

## Linting & Formatting


This project uses [eslint](https://eslint.org/) and [prettier](https://prettier.io/) for linting and formatting. Eslint is configured using [tanstack/eslint-config](https://tanstack.com/config/latest/docs/eslint). The following scripts are available:

```bash
Expand All @@ -47,7 +117,6 @@ pnpm format
pnpm check
```


## Deploy with Nitro

This project uses Nitro as a generic server adapter, so it can run on any Node-compatible host.
Expand All @@ -61,8 +130,6 @@ The build output is a self-contained Node server. To deploy, push the `dist/` di

For host-specific presets (Vercel, Netlify, Cloudflare, AWS Lambda, etc.) and tuning, see https://v3.nitro.build/deploy.



## Routing

This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`.
Expand All @@ -80,7 +147,7 @@ Now that you have two routes you can use a `Link` component to navigate between
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.

```tsx
import { Link } from "@tanstack/react-router";
import { Link } from '@tanstack/react-router'
```

Then anywhere in your JSX you can use it like so:
Expand Down Expand Up @@ -148,11 +215,11 @@ const getServerTime = createServerFn({
// Use in a component
function MyComponent() {
const [time, setTime] = useState('')

useEffect(() => {
getServerTime().then(setTime)
}, [])

return <div>Server time: {time}</div>
}
```
Expand Down Expand Up @@ -205,12 +272,10 @@ function PeopleComponent() {

Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).


# Demo files

Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.


# Learn More

You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# PostgreSQL connection string used by the Prisma client at runtime
DATABASE_URL="postgresql://username:password@localhost:5432/mydb"
23 changes: 23 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@t09-project-2/backend",
"private": true,
"type": "module",
"scripts": {
"dev": "dotenv -e .env -- tsx watch src/index.ts",
"start": "dotenv -e .env -- tsx src/index.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@apollo/server": "^5.5.1",
"@pothos/core": "^4.0.0",
"@pothos/plugin-prisma": "^4.0.0",
"@t09-project-2/db": "workspace:*",
"graphql": "^16.9.0"
},
"devDependencies": {
"@types/node": "^22.20.4",
"dotenv-cli": "^11.0.0",
"tsx": "^4.23.15",
"typescript": "^6.0.3"
}
}
29 changes: 29 additions & 0 deletions apps/backend/src/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import SchemaBuilder from '@pothos/core'
import PrismaPlugin from '@pothos/plugin-prisma'

import { getDatamodel, prisma } from '@t09-project-2/db'
import type { PrismaTypes } from '@t09-project-2/db'

import type { Context } from './context.js'

export const builder = new SchemaBuilder<{
Context: Context
PrismaTypes: PrismaTypes
Scalars: {
DateTime: {
Input: Date
Output: Date
}
}
}>({
plugins: [PrismaPlugin],
prisma: {
client: prisma,
dmmf: getDatamodel(),
onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn',
},
})

builder.scalarType('DateTime', {
serialize: (date: Date) => date.toISOString(),
})
10 changes: 10 additions & 0 deletions apps/backend/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { prisma } from '@t09-project-2/db'
import type { PrismaClient } from '@t09-project-2/db'

export interface Context {
prisma: PrismaClient
}

export function createContext(): Context {
return { prisma }
}
17 changes: 17 additions & 0 deletions apps/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'

import { builder } from './builder.js'
import { createContext } from './context.js'
import './schema/todo.js'

const server = new ApolloServer({
schema: builder.toSchema(),
})

const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async () => createContext(),
})

console.log(`GraphQL server ready at ${url}`)
42 changes: 42 additions & 0 deletions apps/backend/src/schema/todo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { prisma } from '@t09-project-2/db'

import { builder } from '../builder.js'

builder.prismaObject('Todo', {
fields: (t) => ({
id: t.exposeInt('id'),
title: t.exposeString('title'),
createdAt: t.expose('createdAt', { type: 'DateTime' }),
}),
})

builder.queryType({
fields: (t) => ({
todos: t.prismaField({
type: ['Todo'],
resolve: (query) =>
prisma.todo.findMany({ ...query, orderBy: { createdAt: 'desc' } }),
}),
}),
})

builder.mutationType({
fields: (t) => ({
createTodo: t.prismaField({
type: 'Todo',
args: {
title: t.arg.string({ required: true }),
},
resolve: (query, _root, { title }) =>
prisma.todo.create({ ...query, data: { title } }),
}),
deleteTodo: t.prismaField({
type: 'Todo',
args: {
id: t.arg.int({ required: true }),
},
resolve: (query, _root, { id }) =>
prisma.todo.delete({ ...query, where: { id } }),
}),
}),
})
22 changes: 22 additions & 0 deletions apps/backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"include": ["src"],

"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": ["node"],

"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,

"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
}
40 changes: 40 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@t09-project-2/frontend",
"private": true,
"type": "module",
"imports": {
"#/*": "./src/*"
},
"scripts": {
"dev": "vite dev --port 3000",
"generate-routes": "tsr generate",
"build": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.3",
"@tanstack/react-devtools": "latest",
"@tanstack/react-query": "latest",
"@tanstack/react-query-devtools": "latest",
"@tanstack/react-router": "latest",
"@tanstack/react-router-devtools": "latest",
"@tanstack/react-router-ssr-query": "latest",
"@tanstack/react-start": "latest",
"nitro": "3.0.260610-beta",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"tailwindcss": "^4.3.3"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.20",
"@tanstack/devtools-vite": "latest",
"@tanstack/router-cli": "^1.167.39",
"@types/node": "^22.20.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
"vite": "^8.3.1"
}
}
File renamed without changes
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ export default function Header() {
</summary>
<div className="mt-2 min-w-56 rounded-xl border border-[var(--line)] bg-[var(--header-bg)] p-2 shadow-lg sm:absolute sm:right-0">
<a
href="/demo/prisma"
href="/demo/todos"
className="block rounded-lg px-3 py-2 text-sm text-[var(--sea-ink-soft)] no-underline transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]"
>
Prisma
Todos
</a>
<a
href="/demo/tanstack-query"
Expand Down
Loading