Skip to content

A demo backend showing how to combine Effect, Apollo GraphQL, and Drizzle ORM for robust, type-safe APIs with functional error handling

Notifications You must be signed in to change notification settings

Mumma6/effect-graphql-drizzle

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

17 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Effect + Apollo GraphQL + Drizzle Demo

Link to Dev post

This project demonstrates how to build a robust, type-safe backend using Effect, Apollo GraphQL, and Drizzle ORM.

Features

  • Functional, effectful business logic with Effect
  • Type-safe database access with Drizzle ORM
  • GraphQL API with Apollo Server
  • SQLite as the database (file-based, easy for demos)
  • Robust error handling, logging, and separation of concerns
  • Branded types for type-safe IDs (see domain/ticket/schema.ts)

Project Structure

  • domain/ - All business logic, models, schemas, errors, and branded types.
    • ticket/model.ts - Drizzle table definition for tickets.
    • ticket/schema.ts - Input validation schemas, including a branded TicketId type for type-safe IDs.
    • ticket/service.ts - Business logic (e.g., finding, creating tickets).
    • ticket/repository.ts - Database access functions.
    • errors.ts - Domain-specific error types.
  • lib/ - Database setup and utilities.
    • db.ts - Sets up the SQLite/Drizzle/Effect database connection and layers.
    • utils.ts - Response helpers for GraphQL (e.g., successResponse, errorResponse).
  • scripts/ - Utility scripts.
    • seed.ts - Seeds the database with demo data and creates the table if needed.
  • graphql/ - GraphQL schema and resolvers.
    • schema/typeDefs.ts - GraphQL SDL schema (types, queries, mutations).
    • resolvers/index.ts - GraphQL resolvers. This is where all error handling, retries, timeouts, and mapping from domain/service results to GraphQL responses happens. The resolver layer is responsible for API robustness: it catches and logs errors, applies retry policies, handles timeouts, and translates domain/service errors into GraphQL-friendly responses.
  • server.ts - App entrypoint. Sets up ApolloServer, checks DB/table existence, and starts the API.

Getting Started

  1. Install dependencies:
    npm install
  2. Seed the database:
    npm run seed
  3. Start the server:
    npm run dev

Resetting the Database

To delete the database file and reseed:

npm run db:reset

Tech Stack

Resolvers

Queries

findAll

Retrieves root-level tickets (tickets with no parent) with pagination support.

Input: FindAllTicketsInput { offset: Int!, limit: Int! }

Behavior:

  • Returns only tickets where parentId is null (root tickets)
  • Supports pagination with offset and limit parameters
  • Each root ticket is enriched with its complete child tree
  • Uses parallel processing to enrich multiple tickets simultaneously

Example Response:

{
  "success": true,
  "data": [
    {
      "id": "1",
      "title": "Project A - Main Project",
      "children": [
        {
          "id": "4",
          "title": "Frontend Development",
          "children": [...]
        }
      ]
    }
  ],
  "message": "πŸ“‹ Retrieved 3 root tickets"
}

findById

Retrieves a specific ticket by ID along with its complete child tree.

Input: FindTicketByIdInput { id: ID! }

Behavior:

  • Finds the ticket with the specified ID
  • Enriches the ticket with its complete child tree (up to 10 levels deep)
  • Returns detailed information about the ticket's hierarchy
  • Includes count of direct children and total descendants

Example Response:

{
  "success": true,
  "data": {
    "id": "1",
    "title": "Project A - Main Project",
    "children": [
      {
        "id": "4",
        "title": "Frontend Development",
        "children": [...]
      }
    ]
  },
  "message": "🌳 Found ticket \"Project A - Main Project\" (ID: 1) with 3 direct children and 15 total descendants"
}

Mutations

createTicket

Creates a new root-level ticket.

Input: CreateTicketInput { title: String!, description: String! }

Behavior:

  • Creates a new ticket with the provided title and description
  • Sets completed to false by default
  • Sets parentId to null (creates a root ticket)
  • Automatically sets createdAt and updatedAt timestamps
  • Returns the newly created ticket

Example Response:

{
  "success": true,
  "data": {
    "id": "21",
    "title": "New Feature Implementation",
    "description": "Implement user authentication system",
    "completed": false,
    "parentId": null
  },
  "message": "βœ… Successfully created ticket \"New Feature Implementation\" with ID: 21"
}

toggleTicket

Toggles the completion status of a ticket and all its descendants (cascade toggle).

Input: ToggleTicketInput { id: ID!, isCompleted: Boolean! }

Behavior:

  • Finds the specified ticket and toggles its completion status
  • Recursively finds all child tickets in the tree
  • Updates the completion status of the parent ticket and all descendants
  • Uses parallel processing for efficiency when updating multiple tickets
  • Returns the complete updated ticket tree

Example: Before toggle (ID: 1, isCompleted: true):

Root (ID: 1, completed: false)
β”œβ”€β”€ Child (ID: 2, completed: false)
β”‚   β”œβ”€β”€ Grandchild (ID: 3, completed: false)
β”‚   └── Grandchild (ID: 4, completed: false)
└── Child (ID: 5, completed: false)

After toggle:

Root (ID: 1, completed: true)
β”œβ”€β”€ Child (ID: 2, completed: true)
β”‚   β”œβ”€β”€ Grandchild (ID: 3, completed: true)
β”‚   └── Grandchild (ID: 4, completed: true)
└── Child (ID: 5, completed: true)

Example Response:

{
  "success": true,
  "data": {
    "id": "1",
    "title": "Project A - Main Project",
    "completed": true,
    "children": [...]
  },
  "message": "βœ… Updated completion status for \"Project A - Main Project\" and 5 child tickets to: true"
}

deleteTicket

Deletes a ticket and all its descendants (cascade delete).

Input: DeleteTicketInput { id: ID! }

Behavior:

  • Finds the specified ticket and its complete child tree
  • Deletes the parent ticket and all descendants
  • Uses parallel processing for efficient deletion of multiple tickets
  • Returns information about all deleted tickets

Example: Before delete (ID: 1):

Root (ID: 1)
β”œβ”€β”€ Child (ID: 2)
β”‚   β”œβ”€β”€ Grandchild (ID: 3)
β”‚   └── Grandchild (ID: 4)
└── Child (ID: 5)

After delete:

(All tickets 1, 2, 3, 4, 5 are deleted)

Example Response:

{
  "success": true,
  "data": [
    {"deletedId": "1"},
    {"deletedId": "2"},
    {"deletedId": "3"},
    {"deletedId": "4"},
    {"deletedId": "5"}
  ],
  "message": "πŸ—‘οΈ Successfully deleted ticket and 4 child tickets (5 total tickets removed)"
}

removeParentFromTicket

Converts a child ticket to a root-level ticket by removing its parent relationship.

Input: RemoveParentInput { id: ID! }

Behavior:

  • Finds the specified ticket and checks if it has a parent
  • If the ticket is already a root ticket, returns it unchanged
  • If the ticket has a parent, removes the parent relationship (sets parentId to null)
  • The ticket and all its children become a separate root tree
  • Returns the updated ticket with information about affected children

Example: Before removeParent(ID: 3):

Root (ID: 1)
β”œβ”€β”€ Child (ID: 2) 
β”‚   β”œβ”€β”€ Grandchild (ID: 3) ← This ticket
β”‚   β”‚   β”œβ”€β”€ Great-grandchild (ID: 4)
β”‚   β”‚   └── Great-grandchild (ID: 5)
β”‚   └── Grandchild (ID: 6)

After removeParent(ID: 3):

Root (ID: 1)
β”œβ”€β”€ Child (ID: 2) 
β”‚   └── Grandchild (ID: 6)

Root (ID: 3) ← Now a root with its children
β”‚   β”œβ”€β”€ Child(ID: 4)
β”‚   └── Child(ID: 5)

Example Response:

{
  "success": true,
  "data": {
    "id": "3",
    "title": "Frontend Components",
    "parentId": null
  },
  "message": "βœ… Successfully removed parent from ticket \"Frontend Components\" (ID: 3) along with 2 child tickets. Ticket is now a root ticket."
}

setParentOfTicket

Moves a ticket and all its descendants to become children of another ticket.

Input: SetParentInput { childId: ID!, parentId: ID! }

Behavior:

  • Validates that both child and parent tickets exist
  • Prevents setting a ticket as its own parent
  • Checks for circular relationships (prevents creating cycles in the hierarchy)
  • Moves the entire subtree: the child ticket and all its descendants
  • Updates the child ticket's parentId to point to the new parent
  • Returns the updated child ticket with information about the new parent and total tickets moved

Validation Rules:

  • Both childId and parentId must exist
  • childId cannot equal parentId
  • Cannot create circular relationships (e.g., A β†’ B β†’ C β†’ A)

Example: Before setParent(childId: 3, parentId: 5):

Root (ID: 1)
β”œβ”€β”€ Child (ID: 2)
β”‚   └── Grandchild (ID: 3) ← This ticket and its subtree
β”‚       β”œβ”€β”€ Great-grandchild (ID: 4)
β”‚       └── Great-grandchild (ID: 5)

Root (ID: 6)
└── Child (ID: 7) ← Target parent

After setParent(childId: 3, parentId: 7):

Root (ID: 1)
└── Child (ID: 2)

Root (ID: 6)
└── Child (ID: 7)
    └── Grandchild (ID: 3) ← Moved here with entire subtree
        β”œβ”€β”€ Great-grandchild (ID: 4)
        └── Great-grandchild (ID: 5)

Example Response:

{
  "success": true,
  "data": {
    "id": "3",
    "title": "Frontend Components",
    "parentId": "7"
  },
  "message": "βœ… Successfully moved ticket \"Frontend Components\" (ID: 3) and 2 descendants to \"Backend API\" (ID: 7)"
}

addChildrenToTicket

Adds multiple tickets as children to a parent ticket in a single operation.

Input: AddChildrenInput { parentId: ID!, childrenIds: [ID!]! }

Behavior:

  • Validates that the parent ticket exists
  • Validates that all child tickets exist
  • Checks for circular relationships (prevents creating cycles in the hierarchy)
  • Prevents duplicate child IDs in the input
  • Checks if any child is already a child of the parent
  • Moves all children to the parent using parallel processing for efficiency
  • Returns information about the parent and all moved children

Validation Rules:

  • parentId must exist
  • All childrenIds must exist
  • No duplicate IDs in childrenIds array
  • Cannot create circular relationships (e.g., A β†’ B β†’ C β†’ A)
  • Children cannot already be children of the parent
  • Maximum 50 children per operation (performance limit)

Example: Before addChildren(parentId: 5, childrenIds: [3, 7, 9]):

Root (ID: 1)
β”œβ”€β”€ Child (ID: 2)
β”‚   └── Grandchild (ID: 3) ← This ticket
β”‚       └── Great-grandchild (ID: 4)

Root (ID: 6)
└── Child (ID: 7) ← This ticket

Root (ID: 8)
└── Child (ID: 9) ← This ticket

Root (ID: 10)
└── Child (ID: 5) ← Target parent

After addChildren(parentId: 5, childrenIds: [3, 7, 9]):

Root (ID: 1)
└── Child (ID: 2)

Root (ID: 6)

Root (ID: 8)

Root (ID: 10)
└── Child (ID: 5) ← Target parent
    β”œβ”€β”€ Child (ID: 3) ← Moved here with its subtree
    β”‚   └── Child (ID: 4)
    β”œβ”€β”€ Child (ID: 7) ← Moved here
    └── Child (ID: 9) ← Moved here

Example Response:

{
  "success": true,
  "data": {
    "id": "5",
    "title": "Backend API",
    "parentId": "10"
  },
  "message": "βœ… Successfully added 3 children (\"Frontend Components\", \"Database Design\", \"User Authentication\") to \"Backend API\" (ID: 5)"
}

About

A demo backend showing how to combine Effect, Apollo GraphQL, and Drizzle ORM for robust, type-safe APIs with functional error handling

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published