# 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: ```graphql 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: ```graphql 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: ```graphql 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: ```graphql 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: ```graphql 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: ```graphql 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 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) ```graphql 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