That blog post is great for more senior programmers and people who have time to roll their own integration. But what about those who just want to say “boom, Stripe”?
It would be great if Stripe provided an official Elixir SDK, but they don’t. The community project Stripity Stripe attempts to fill that gap. It has worked great for many people for a long time, but since the Stripe API is so sprawling it’s been cumbersome for maintainers and new code hasn’t been merged in over a year. This can give newcomers pause.
PinStripe aspires to find a middle ground by providing a maintainable, minimalistic interface with a few conveniences that should cover 95% of use cases without needing to meticulously cover the entire Stripe SDK.
Features include:
Simple API Client built on Req with automatic ID prefix recognition
Webhook Handler DSL using Spark for clean, declarative webhook handling
Automatic Signature Verification for webhook security
Code Generators powered by Igniter for zero-config setup
Sync with Stripe to keep your local handlers in sync with your Stripe dashboard
Please note this library is still experimental! Tests are passing, igniter installation and mix tasks all seem to work! But community feedback and contributions are very welcome.
Gonna wait to see if more suggestions roll in before I pull the trigger on that but that’s great. Fun, fashionable, rolls of the tongue, and yet also suggests professionalism.
test "reads a customer using stub_read" do
Mock.stub_read("cus_123", %{"id" => "cus_123", "email" => "test@example.com"})
{:ok, response} = Client.read("cus_123")
assert response.body["email"] == "test@example.com"
end
test "creates a customer" do
# Load/generate a customer fixture (use atoms for API resources)
customer = PinStripe.Test.Fixtures.load(:customer)
# Use with Req.Test
Req.Test.stub(PinStripe, fn conn ->
Req.Test.json(conn, customer)
end)
{:ok, response} = Client.create(:customers, %{email: "test@example.com"})
assert response.body["object"] == "customer"
end
Fixture generation requires a Stripe test API Key (live keys are not allowed). Each unique generated fixture will create a record in your Stripe dev dashboard and a cached version in test/fixtures/stripe.
If/when you change your Stripe API version, you can run mix pin_stripe.sync_api_version to fetch the current version and clear all locally cached fixtures. Fixtures will then be regenerated the next time you run your tests.
Spark is what generates this very simple DSL for the WebhookHandler module:
defmodule MyApp.StripeWebhookHandlers do
use PinStripe.WebhookHandler
handle "customer.created", MyApp.StripeWebhookHandlers.CustomerCreated
handle "customer.updated", fn data ->
// do stuff
end
end
That part is actually not in the Dashbit blog. They just do:
defmodule TeamsWeb.StripeWebhookController do
use TeamsWeb, :controller
plug :verify_signature
def create(conn, %{"type" => type}) do
# ...
end
defp verify_signature(conn, []) do
# ...
end
end
… the idea being that a dev reading it knows “ahh, I put some handling code in the create function.”
Whereas PinStripe’s WebHook controller is literally just this:
defmodule MyAppWeb.StripeWebhookController do
use PinStripe.WebhookController,
handler: MyApp.StripeWebhookHandlers
end
That injects all the sig verification code from the Dashbit controller and forwards webhook events to the WebhookHandler module.
So PinStripe is more opinionated, but it saves some fussy setup.
It’s actually possible though that the handle "stripe.event" ... DSL could just live right there in the controller though
Then it would just be:
defmodule MyAppWeb.StripeWebhookController do
use PinStripe.WebhookController
handle "customer.created", fn(data) -> ... end
handle "customer.updated", MyApp.StripeWebhookHandlers.CustomerUpdated
end
Might do that! Not sure there’s a need to have the handler declarations live in a separate module. It would save a bit of indirection.
Webhook event handlers are now defined directly in your WebhookController instead of a separate WebhookHandler module. This reduces indirection and simplifies the overall architecture.
Before (0.2.x):
defmodule MyApp.StripeWebhookHandlers do
use PinStripe.WebhookHandler
handle "customer.created", fn event -> ... end
end
defmodule MyAppWeb.StripeWebhookController do
use PinStripe.WebhookController,
handler: MyApp.StripeWebhookHandlers
end
After (0.3.0):
defmodule MyAppWeb.StripeWebhookController do
use PinStripe.WebhookController
handle "customer.created", fn event -> ... end
end
Migration
Move your handle declarations from your WebhookHandler module into your WebhookController. Handler modules (for complex handlers) should remain in lib/my_app/stripe_webhook_handlers/ but are now referenced directly from the controller.
Probably 6-7 years back I worked on a client project that had stripity stripe and they were rawdogging everything and had very little in terms of tests. I needed to modify billing so I wanted tests. And just being able to test behavior manually against Stripe.
Oh yeah! So actually the Dashbit article has an example of that too where it automatically starts the Stripe listener in dev mode and points it at the webhook endpoint. It’s on my short list to add that to the installer.
Also fwiw PinStripe does currently generate fixtures for webhhooks. Fixture gen uses the Stripe CLI, which means fixtures create a record in the Stripe dev dashboard when they’re first generated. After that though they are cached locally or, should I say, fixed.
Since I am implementing Stripe checkout in a Phoenix LiveView app, I’m curious how PinStripe’s approach differs from an approach like this - Setup Stripe with Phoenix LiveView · FullstackPhoenix. I’m planning on using Stripe Elements for the checkout UI and am also using Ash Framework.