gql2sql is a teaching-focused example of how to build a modern GraphQL API on top of SQL without stopping at the database. The app exposes books from SQLite through Prisma, combines that with poetry data from a public API, and keeps the codebase structured the way current TypeScript projects are usually built.
- Apollo Server 5 mounted on Express 5
- Prisma ORM 7 with SQLite and driver adapters
- strict TypeScript with generated GraphQL resolver types
- Biome for formatting and linting
- GraphQL SDL in
.graphqlfiles instead of inline strings - HTTP-level integration tests with Vitest and Supertest
- agent-friendly repository conventions via
AGENTS.md
- Node 22 or newer
- npm 10 or newer
The request flow is intentionally simple and explicit:
- Express receives HTTP requests and Apollo handles GraphQL execution at
/graphql. - SDL files in
src/graphql/schemadefine the public contract. - Typed resolvers translate GraphQL operations into service calls.
- Services coordinate domain rules and delegate persistence to repositories.
- Prisma talks to SQLite for books, while a dedicated poetry client fetches a second upstream data source.
This separation keeps the repo readable for learners and gives coding agents a clear place to make targeted changes.
src/server.ts: startup, env loading, graceful shutdownsrc/app.ts: Express and Apollo compositionsrc/config: environment parsing and loggingsrc/graphql: SDL files, generated resolver types, and resolverssrc/services: domain services and the poetry API clientsrc/data: Prisma client factory and repositoriesprisma: schema, migrations, and seed script
-
Install dependencies:
npm install
-
Copy the example environment file:
cp .env.example .env
-
Generate the Prisma client, apply the committed migration, and seed the database:
npm run db:generate npm run db:migrate npm run db:seed
-
Start the development server:
npm run dev
-
Open
http://localhost:3000/graphql.
Apollo Sandbox is enabled in development, so you can browse the schema and execute operations directly in the browser.
Query all books:
query GetBooks {
books {
id
title
author
createdAt
}
}Query one book by id:
query GetBook($id: ID!) {
book(id: $id) {
id
title
author
}
}Create a book with a modern input object:
mutation AddBook($input: AddBookInput!) {
addBook(input: $input) {
id
title
author
}
}Variables:
{
"input": {
"title": "Kindred",
"author": "Octavia E. Butler"
}
}Query poems from the external provider:
query GetPoetry {
poem {
title
content
lineCount
poet {
name
}
}
poems {
title
poet {
name
}
}
}
npm run dev: watch mode withtsxnpm run build: generate Prisma client, run GraphQL codegen, and compile TypeScriptnpm run typecheck: strict TypeScript verificationnpm run lint: Biome checksnpm run format: Biome formattingnpm run test: Vitest suitenpm run db:generate: Prisma client generationnpm run db:migrate: apply the committed migration setnpm run db:reset: reset the local databasenpm run db:seed: seed the databasenpm run db:studio: open Prisma Studio
Run the same checks expected in CI:
npm run lint
npm run typecheck
npm run testFor a clean local reset:
npm run db:reset
npm run db:seed- Keep schema changes in
.graphqlfiles and regenerate types withnpm run codegen. - Keep persistence logic out of resolvers.
- If the Prisma schema changes, rerun
npm run db:generateand update migrations. - To author a brand-new Prisma migration locally, use
npx prisma migrate dev --name <change>on Node 22 and commit the generated SQL. - See
AGENTS.mdfor repository-specific coding guidance.

