This article was originally published on Rails Designer's build a SaaS series
For a current SaaS I am working on I needed to set up a webhook endpoint. I want the production environment to use a subdomain like webhooks.helptail.com
, but during development, this needs to work on localhost:3000
. So I used some of Rails' routing features for a solution that adapts based on the environment.
The Challenge with Subdomains Locally
When developing locally, Rails applications typically run on localhost:3000
, which doesn't support subdomains in the same way as (development) domain name, like a .dev
TLD.
To solve this problem, a different routing strategy is needed for development versus production environments.
Here's a clean solution that handles both scenarios elegantly:
# config/routes.rb constraints Rails.env.production? ? {subdomain: "webhooks"} : {} do scope Rails.env.production? ? {} : {path: "webhooks"} do post "/:path", to: "workflow/webhooks#create", as: :webhooks end end
This code block does two things:
- Production: routes requests to the
webhooks
subdomain; example:https://webhooks.helptail.com/a-unique-path
- Development/test: adds a
/webhooks
prefix to the path; example:http://localhost:3000/webhooks/a-unique-path
The “magic” happens through the conditional use of Rails' constraints
and scope
methods. The constraints
method applies the subdomain restriction only in production, while the scope
method adds the path prefix only in development.
Using separate subdomains for APIs and webhooks (like webhooks.helptail.com
) creates a foundation for growth. This approach lets you scale high-traffic parts of your app independently when customer usage surges, implement targeted security policies without complicating your main application (eg. dashboard.helptail.com
), and establishes an architecture that simplifies future migrations as you expand (globally through CDNs).
This same pattern works great for API endpoints too. Many applications use an api
subdomain:
constraints Rails.env.production? ? {subdomain: "api"} : {} do scope Rails.env.production? ? {} : {path: "api"}, defaults: {format: :json} do namespace :v1 do resources :users resources :posts end end end
This creates endpoints like:
- Production:
https://api.example.com/v1/users
- Development:
http://localhost:3000/api/v1/users
What do you think of this solution?
Top comments (0)