Files
clubber/openspec/changes/archive/2025-11-20-add-graphql-member-api/specs/graphql-api/spec.md
T
gurixandClaude 5bb27bdadf chore: archive add-graphql-member-api proposal and create capability specs
Archive completed GraphQL API implementation proposal:
- Move proposal to openspec/changes/archive/2025-11-20-add-graphql-member-api/
- Create capability specs in openspec/specs/:
  - database-layer: SQLAlchemy async with Alembic migrations
  - graphql-api: Strawberry GraphQL with FastAPI integration
  - member-crud: Member management with conditional validation
  - project-setup: Python 3.11+ with uv package manager

All 73 tasks completed and validated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 13:26:43 +01:00

11 KiB

Spec: GraphQL API

Capability: graphql-api Status: Draft Last Updated: 2025-11-20

Overview

This capability defines the GraphQL API layer using Strawberry GraphQL integrated with FastAPI, providing type-safe schema, queries, and mutations for member management.

ADDED Requirements

Requirement: Strawberry GraphQL schema with Member type

The system MUST define a GraphQL schema using Strawberry with Member object type mapping to database model fields.

Member GraphQL type fields:

  • id: ID! (non-null unique identifier)
  • firstName: String! (non-null)
  • lastName: String (nullable)
  • street: String (nullable)
  • apartmentNumber: String (nullable)
  • zip: String (nullable)
  • city: String (nullable)
  • country: String (nullable)
  • email: String (nullable)
  • phone: String (nullable)
  • createdAt: DateTime! (non-null timestamp)
  • updatedAt: DateTime! (non-null timestamp)

Scenario: Member type is defined with Strawberry decorator

Given src/schemas/member.py exists When Member class is decorated with @strawberry.type Then GraphQL type "Member" is registered in schema And all fields use camelCase naming (GraphQL convention) And field types match SQLAlchemy model types And snake_case database fields map to camelCase GraphQL fields

Scenario: GraphQL schema is introspectable

Given FastAPI app with GraphQL endpoint is running When client accesses /graphql endpoint Then GraphiQL playground is displayed And schema introspection reveals Member type And all fields and their types are documented

Requirement: Input types for mutations

The system MUST define separate input types for create and update operations with appropriate field requirements.

CreateMemberInput fields (only firstName required):

  • firstName: String!
  • lastName: String
  • street: String
  • apartmentNumber: String
  • zip: String
  • city: String
  • country: String
  • email: String
  • phone: String

UpdateMemberInput fields (id required, all others optional):

  • id: ID!
  • firstName: String
  • lastName: String
  • street: String
  • apartmentNumber: String
  • zip: String
  • city: String
  • country: String
  • email: String
  • phone: String

Scenario: CreateMemberInput enforces required fields

Given CreateMemberInput type is defined When client sends mutation without firstName Then GraphQL validation error is returned And error message indicates firstName is required When client sends mutation with only firstName Then input validation passes And member is created with only firstName populated

Scenario: UpdateMemberInput allows partial updates

Given UpdateMemberInput type is defined When client sends mutation with only id and email Then only email field is updated in database And all other fields remain unchanged When client sends mutation without id Then GraphQL validation error is returned

Requirement: Query resolvers for reading members

The system MUST provide GraphQL query resolvers for fetching individual members and listing all members.

Query operations:

  • member(id: ID!): Member - Get single member by ID
  • members: [Member!]! - List all members

Scenario: Query single member by ID

Given member exists with id=1 When client executes query:

query {
  member(id: 1) {
    id
    firstName
    lastName
    email
  }
}

Then response contains member data And response matches GraphQL Member type structure

Scenario: Query member with non-existent ID

Given no member exists with id=999 When client executes query member(id: 999) Then response returns null for member field And no error is raised (null is valid for nullable return)

Scenario: List all members

Given multiple members exist in database When client executes query:

query {
  members {
    id
    firstName
    lastName
  }
}

Then response contains array of all members And members are sorted by last name, first name When no members exist Then response contains empty array

Requirement: Mutation resolvers for modifying members

The system MUST provide GraphQL mutation resolvers for creating, updating, and deleting members.

Mutation operations:

  • createMember(input: CreateMemberInput!): Member! - Create new member
  • updateMember(input: UpdateMemberInput!): Member! - Update existing member
  • deleteMember(id: ID!): Boolean! - Delete member

Scenario: Create new member mutation

Given valid CreateMemberInput is provided When client executes mutation:

mutation {
  createMember(input: {
    firstName: "Jane"
    lastName: "Smith"
    street: "123 Main St"
    zip: "12345"
    city: "Springfield"
    country: "USA"
    email: "jane@example.com"
    phone: "+15551234567"
  }) {
    id
    firstName
    email
  }
}

Then new member is persisted to database And response contains newly created member with generated id And createdAt and updatedAt are populated

Scenario: Create member with minimal data (firstName only)

Given CreateMemberInput with only firstName is provided When client executes mutation:

mutation {
  createMember(input: {
    firstName: "John"
  }) {
    id
    firstName
    lastName
    email
  }
}

Then new member is created in database And firstName is "John" And lastName, email, phone, and address fields are null And response contains member with null optional fields

Scenario: Create member with invalid input

Given CreateMemberInput has invalid email format When client executes createMember mutation Then GraphQL error is returned And error message indicates validation failure And no database record is created

Scenario: Update existing member mutation

Given member exists with id=1 When client executes mutation:

mutation {
  updateMember(input: {
    id: 1
    email: "newemail@example.com"
    phone: "+15559876543"
  }) {
    id
    email
    phone
    updatedAt
  }
}

Then member email and phone are updated in database And updatedAt timestamp is refreshed And all other fields remain unchanged

Scenario: Update non-existent member

Given no member exists with id=999 When client executes updateMember with id=999 Then GraphQL error is returned And error message indicates "Member not found"

Scenario: Delete member mutation

Given member exists with id=1 When client executes mutation:

mutation {
  deleteMember(id: 1)
}

Then member is removed from database And response returns true When query attempts to fetch deleted member Then response returns null

Scenario: Delete non-existent member

Given no member exists with id=999 When client executes deleteMember(id: 999) Then GraphQL error is returned And error message indicates "Member not found"

Requirement: FastAPI integration with GraphQL endpoint

The system MUST integrate Strawberry GraphQL schema with FastAPI application at /graphql path with GraphiQL playground enabled.

Scenario: GraphQL endpoint is mounted on FastAPI app

Given src/main.py creates FastAPI app When Strawberry schema is created from Query and Mutation classes Then GraphQLRouter is created with schema And router is mounted at /graphql path And GraphiQL is enabled for development

Scenario: GraphQL playground is accessible

Given FastAPI app is running When browser navigates to http://localhost:8000/graphql Then GraphiQL interface is displayed And schema documentation is available And queries can be executed interactively

Scenario: GraphQL endpoint accepts POST requests

Given GraphQL endpoint is configured When client sends POST to /graphql with query in body Then query is executed against schema And JSON response is returned with data or errors

Requirement: Error handling with meaningful messages

The system MUST provide clear, actionable error messages for validation failures, not found errors, and server errors.

Scenario: Input validation error provides field-level details

Given createMember mutation receives invalid email When mutation executes Then GraphQL error includes message "Invalid email format" And error path indicates which input field failed And HTTP status code is 400 (Bad Request)

Scenario: Not found error provides resource context

Given member query requests non-existent id=999 When query executes Then error message is "Member with ID 999 not found" And error type indicates resource not found

Scenario: Database error is handled gracefully

Given database connection fails during query When query executes Then GraphQL error indicates server error And internal error details are logged And client receives generic "Internal server error" message And HTTP status code is 500

MODIFIED Requirements

None (new capability)

REMOVED Requirements

None (new capability)

Cross-References

Depends On

  • project-setup - Requires FastAPI and Strawberry dependencies
  • database-layer - Requires Member model and database sessions

Enables

  • member-crud - Provides API interface for business operations

None

Implementation Notes

  1. Naming convention: Use camelCase for GraphQL fields (firstName), snake_case for Python (first_name)
  2. Type conversion: Strawberry automatically converts between Python and GraphQL types
  3. Async resolvers: All resolvers must be async functions for database operations
  4. Dependency injection: Use FastAPI's Depends() for database session in resolvers
  5. Error handling: Raise Python exceptions, Strawberry converts to GraphQL errors
  6. GraphiQL: Disable in production by setting graphiql=False

GraphQL Schema (SDL Reference)

type Member {
  id: ID!
  firstName: String!
  lastName: String
  street: String
  apartmentNumber: String
  zip: String
  city: String
  country: String
  email: String
  phone: String
  createdAt: DateTime!
  updatedAt: DateTime!
}

input CreateMemberInput {
  firstName: String!
  lastName: String
  street: String
  apartmentNumber: String
  zip: String
  city: String
  country: String
  email: String
  phone: String
}

input UpdateMemberInput {
  id: ID!
  firstName: String
  lastName: String
  street: String
  apartmentNumber: String
  zip: String
  city: String
  country: String
  email: String
  phone: String
}

type Query {
  member(id: ID!): Member
  members: [Member!]!
}

type Mutation {
  createMember(input: CreateMemberInput!): Member!
  updateMember(input: UpdateMemberInput!): Member!
  deleteMember(id: ID!): Boolean!
}

Validation Checklist

  • Member Strawberry type defined in src/schemas/member.py
  • CreateMemberInput and UpdateMemberInput defined
  • Query class with member and members resolvers
  • Mutation class with create, update, delete resolvers
  • GraphQL schema created in src/main.py
  • /graphql endpoint mounted on FastAPI app
  • GraphiQL accessible at http://localhost:8000/graphql
  • All queries and mutations execute successfully
  • Error messages are clear and actionable
  • Schema introspection shows all types correctly