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>
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: Stringstreet: StringapartmentNumber: Stringzip: Stringcity: Stringcountry: Stringemail: Stringphone: String
UpdateMemberInput fields (id required, all others optional):
id: ID!firstName: StringlastName: Stringstreet: StringapartmentNumber: Stringzip: Stringcity: Stringcountry: Stringemail: Stringphone: 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 IDmembers: [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 memberupdateMember(input: UpdateMemberInput!): Member!- Update existing memberdeleteMember(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
Related To
None
Implementation Notes
- Naming convention: Use camelCase for GraphQL fields (firstName), snake_case for Python (first_name)
- Type conversion: Strawberry automatically converts between Python and GraphQL types
- Async resolvers: All resolvers must be async functions for database operations
- Dependency injection: Use FastAPI's Depends() for database session in resolvers
- Error handling: Raise Python exceptions, Strawberry converts to GraphQL errors
- 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