Note: This was generated by ChatGPT ✨
Great — here’s a clean, production-ready Rails service object pattern for integrating your app with Microsoft Graph (for Outlook / Microsoft To Do / SharePoint).
This pattern gives you:
- Clean separation of concerns
- Automatic token refresh
- Centralized error handling
- Easy background job integration
- Reusable for To Do + SharePoint
🏗 Recommended Structure
app/
services/
microsoft_graph/
base_client.rb
token_refresher.rb
todo_tasks_service.rb
sharepoint_service.rb
1️⃣ Base Client (Handles Auth + HTTP)
app/services/microsoft_graph/base_client.rb
# frozen_string_literal: true
module MicrosoftGraph
class BaseClient
GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0"
def initialize(user)
@user = user
end
private
def connection
@connection ||= Faraday.new(url: GRAPH_BASE_URL) do |f|
f.request :json
f.response :json
f.adapter Faraday.default_adapter
end
end
def access_token
return @user.microsoft_access_token if token_valid?
MicrosoftGraph::TokenRefresher.new(@user).refresh!
@user.reload.microsoft_access_token
end
def token_valid?
@user.microsoft_token_expires_at.present? &&
@user.microsoft_token_expires_at > 5.minutes.from_now
end
def get(path)
request(:get, path)
end
def post(path, body:)
request(:post, path, body)
end
def patch(path, body:)
request(:patch, path, body)
end
def delete(path)
request(:delete, path)
end
def request(method, path, body = nil)
response = connection.send(method) do |req|
req.url path
req.headers["Authorization"] = "Bearer #{access_token}"
req.headers["Content-Type"] = "application/json"
req.body = body if body
end
handle_errors(response)
response.body
end
def handle_errors(response)
return if response.success?
Rails.logger.error("Graph API Error: #{response.status} #{response.body}")
raise "Microsoft Graph API Error: #{response.status}"
end
end
end
2️⃣ Token Refresher Service
app/services/microsoft_graph/token_refresher.rb
# frozen_string_literal: true
module MicrosoftGraph
class TokenRefresher
TOKEN_URL = "https://login.microsoftonline.com/#{ENV['MS_TENANT_ID']}/oauth2/v2.0/token"
def initialize(user)
@user = user
end
def refresh!
response = Faraday.post(TOKEN_URL, {
client_id: ENV["MS_CLIENT_ID"],
client_secret: ENV["MS_CLIENT_SECRET"],
grant_type: "refresh_token",
refresh_token: @user.microsoft_refresh_token,
scope: "offline_access Tasks.ReadWrite Sites.ReadWrite.All"
})
data = JSON.parse(response.body)
@user.update!(
microsoft_access_token: data["access_token"],
microsoft_refresh_token: data["refresh_token"],
microsoft_token_expires_at: Time.current + data["expires_in"].to_i.seconds
)
end
end
end
3️⃣ Microsoft To Do Task Service
app/services/microsoft_graph/todo_tasks_service.rb
# frozen_string_literal: true
module MicrosoftGraph
class TodoTasksService < BaseClient
def create_task(list_id:, title:, due_date: nil)
body = {
title: title
}
if due_date
body[:dueDateTime] = {
dateTime: due_date.iso8601,
timeZone: "UTC"
}
end
post("/me/todo/lists/#{list_id}/tasks", body: body)
end
def update_task(list_id:, task_id:, attributes:)
patch("/me/todo/lists/#{list_id}/tasks/#{task_id}", body: attributes)
end
def delete_task(list_id:, task_id:)
delete("/me/todo/lists/#{list_id}/tasks/#{task_id}")
end
end
end
This syncs with tasks visible in Microsoft Outlook via Microsoft To Do.
4️⃣ SharePoint List Service
app/services/microsoft_graph/sharepoint_service.rb
# frozen_string_literal: true
module MicrosoftGraph
class SharepointService < BaseClient
def create_list_item(site_id:, list_id:, fields:)
post("/sites/#{site_id}/lists/#{list_id}/items", body: { fields: fields })
end
def update_list_item(site_id:, list_id:, item_id:, fields:)
patch("/sites/#{site_id}/lists/#{list_id}/items/#{item_id}/fields", body: fields)
end
end
end
Works with SharePoint lists.
5️⃣ How You Use It in Rails
Example from a model callback:
def sync_to_microsoft!
service = MicrosoftGraph::TodoTasksService.new(user)
response = service.create_task(
list_id: user.microsoft_default_list_id,
title: title,
due_date: due_at
)
update!(microsoft_task_id: response["id"])
end
6️⃣ Recommended: Use Background Jobs
Never sync inside request cycle.
class SyncMicrosoftTaskJob < ApplicationJob
queue_as :default
def perform(action_item_id)
action_item = ActionItem.find(action_item_id)
action_item.sync_to_microsoft!
end
end
🧠 Production-Grade Improvements
If you’re scaling:
✔ Add:
- Retry logic on 429 rate limits
- Webhook subscriptions for two-way sync
- Conflict resolution (store
last_modified_date_time) - Multi-tenant token isolation
- Service-level error classes
🎯 Why This Pattern Works Well
- BaseClient centralizes auth + HTTP
- TokenRefresher isolates OAuth complexity
- Feature services stay clean and readable
- Easy to test with WebMock
- Easy to extend (Calendar, Mail, Teams, etc.)
If you’d like, next I can show:
- A full two-way sync architecture
- Webhook subscription setup
- Or a multi-tenant SaaS-ready structure
Just tell me your deployment model.