Ever wondered how to protect your APi/Backend from Spam? Here is how.
Requirements:
- Elixir v13.3.2 +
- Phoenix v1.6.10+
- Basic Knowledge of Elixir
$ elixir -v Elixir 1.13.2 (compiled with Erlang/OTP 24) $ mix phx.new --version Phoenix installer v1.6.10 Getting started
Create a new Phoenix Project
$ mix phx.new ratelimit --no-mailer --no-assets --no-html --no-ecto --no-dashboard If you will be asked to install some dependencies, answer with y (yes).
Adding Dependencies
Lets add some dependencies that will help us doing all of this
mix.exs
defp deps do [ # here is some other stuff # <-- add the stuff below here --> # http {:httpoison, "~> 1.8"}, # rate limit {:hammer, "~> 6.1"}, ] end For the rate limits, we will use the following library
https://github.com/ExHammer/hammer
Install dependencies
$ mix deps.get Project files
Now lets get our hands dirty by creating some helper functions.
lib/ratelimit/base.ex
defmodule Ratelimit.Base do use HTTPoison.Base @moduledoc """ This handles HTTP requests without api key (basic requests). """ def process_request_headers(headers) do [{"Content-Type", "application/json"} | headers] end end Now lets add a function that gets the IP of the user visiting our website.
lib/ratelimit/helper/getip.ex
defmodule Ratelimit.IP do @doc """ Get the IP address of the current user visiting the route. Formatted as a string: "123.456.78.9" """ # {:ok, String.t()} | {:error, :api_down} @spec getIP() :: {String.t() | :api_down} def getIP() do ip_url = "https://api.ipify.org/" case Ratelimit.Base.get!(ip_url) do %HTTPoison.Response{body: body, status_code: 200} -> body %HTTPoison.Response{status_code: status_code} when status_code > 399 -> IO.inspect(status_code, label: "STATUS_CODE") :error _ -> raise "APi down" end end end Awesome, now lets create a file that handles our ratelimits
lib/ratelimit/util/ratelimit.ex
defmodule RatelimitWeb.Plugs.RateLimiter do import Plug.Conn use RatelimitWeb, :controller alias Ratelimit.IP require Logger # two request / minute are allowed @limit 2 def init(options), do: options def call(conn, _opts) do # call the ip function ip = IP.getIP() case Hammer.check_rate(ip, 60_000, @limit) do {:allow, count} -> assign(conn, :requests_count, count) {:deny, _limit} -> # Beep Boop, remove this in production Logger.debug("Rate limit exceeded for #{inspect(ip)}") error_response(conn) end end defp error_response(conn) do conn |> put_status(:service_unavailable) # set the json status |> json(%{message: "Please wait before sending another request."}) # return an error message |> halt() # stop the process end end Now we have to configure Hammer in our config files.
For that, open config/config.exs and add this line here:
# Config the rate limiter config :hammer, backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]} Adding the Controller
Now we have to create a simple controller for our website.
lib/ratelimit_web/controllers/page_controller.ex
defmodule RatelimitWeb.PageController do use RatelimitWeb, :controller def index(conn, _params) do send_resp(conn, 200, "Hello there!") end end and we have to edit our lib/ratelimit_web/router.ex to the following
pipeline :api do # add the rate limit plug here plug RatelimitWeb.Plugs.RateLimiter plug :accepts, ["json"] end scope "/api", RatelimitWeb do pipe_through :api # add this here get "/test", PageController, :index end Start the Server
Now lets try to start our APi with the following
$ mix phx.server After that, navigate to the following URL:
http://localhost:4000/api/test.
You should see the following:
Sending requests
Now to test our rate limits, send multiple request to the same URL, by just refreshing the page more than 2 times.
You will see something changing suddenly, like this:
๐๐๐ You are awesome!
Additional Things
If you want to change the message, you can easily do this in the lib/ratelimit/util/ratelimit.ex file.
In production, remove the IP inspect at the Logger.debug.
The code can be found here: https://github.com/vKxni/ratelimit


Top comments (0)
Some comments have been hidden by the post's author - find out more