Link to Dev post
This project demonstrates how to build a robust, type-safe backend using Effect, Apollo GraphQL, and Drizzle ORM.
- 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
)
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 brandedTicketId
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.
- Install dependencies:
npm install
- Seed the database:
npm run seed
- Start the server:
npm run dev
To delete the database file and reseed:
npm run db:reset
- Effect (functional effects, error handling, dependency injection)
- Drizzle ORM (type-safe SQL for SQLite)
- Apollo Server (GraphQL API)
- SQLite (file-based database)
- TypeScript
Retrieves root-level tickets (tickets with no parent) with pagination support.
Input: FindAllTicketsInput { offset: Int!, limit: Int! }
Behavior:
- Returns only tickets where
parentId
isnull
(root tickets) - Supports pagination with
offset
andlimit
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"
}
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"
}
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
tofalse
by default - Sets
parentId
tonull
(creates a root ticket) - Automatically sets
createdAt
andupdatedAt
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"
}
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"
}
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)"
}
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
tonull
) - 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."
}
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
andparentId
must exist childId
cannot equalparentId
- 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)"
}
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)"
}