API开发 #

一、API概述 #

1.1 API设计原则 #

  • RESTful设计
  • 版本控制
  • 统一响应格式
  • 错误处理
  • 认证授权

1.2 API目录结构 #

text
lib/hello_web/
├── controllers/
│   └── api/
│       ├── v1/
│       │   ├── user_controller.ex
│       │   └── post_controller.ex
│       └── fallback_controller.ex
├── views/
│   └── api/
│       └── v1/
│           ├── user_view.ex
│           └── post_view.ex
└── router.ex

二、API路由 #

2.1 API管道 #

elixir
defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
    plug :fetch_session
  end

  pipeline :api_auth do
    plug HelloWeb.ApiAuth
  end

  scope "/api/v1", HelloWeb.Api.V1, as: :api_v1 do
    pipe_through :api

    post "/login", SessionController, :create
    post "/register", UserController, :create
  end

  scope "/api/v1", HelloWeb.Api.V1, as: :api_v1 do
    pipe_through [:api, :api_auth]

    resources "/users", UserController, only: [:index, :show, :update]
    resources "/posts", PostController
  end
end

2.2 版本控制 #

elixir
scope "/api/v1", HelloWeb.Api.V1, as: :api_v1 do
  pipe_through :api
  resources "/users", UserController
end

scope "/api/v2", HelloWeb.Api.V2, as: :api_v2 do
  pipe_through :api
  resources "/users", UserController
end

三、JSON响应 #

3.1 基本JSON响应 #

elixir
defmodule HelloWeb.Api.V1.UserController do
  use HelloWeb, :controller

  def index(conn, _params) do
    users = Hello.Accounts.list_users()
    json(conn, %{data: users})
  end

  def show(conn, %{"id" => id}) do
    user = Hello.Accounts.get_user!(id)
    json(conn, %{data: user})
  end
end

3.2 统一响应格式 #

elixir
defmodule HelloWeb.Api.V1.UserController do
  use HelloWeb, :controller

  def index(conn, _params) do
    users = Hello.Accounts.list_users()
    render(conn, "index.json", users: users)
  end
end
elixir
defmodule HelloWeb.Api.V1.UserJSON do
  def index(%{users: users}) do
    %{data: for(user <- users, do: data(user))}
  end

  def show(%{user: user}) do
    %{data: data(user)}
  end

  defp data(user) do
    %{
      id: user.id,
      email: user.email,
      name: user.name,
      inserted_at: user.inserted_at,
      updated_at: user.updated_at
    }
  end
end

3.3 分页响应 #

elixir
def index(conn, params) do
  page = Map.get(params, "page", "1") |> String.to_integer()
  per_page = Map.get(params, "per_page", "10") |> String.to_integer()

  %{entries: users, total_pages: total_pages, page_number: page_number} =
    Hello.Accounts.list_users_paginated(page: page, per_page: per_page)

  json(conn, %{
    data: users,
    meta: %{
      current_page: page_number,
      total_pages: total_pages,
      per_page: per_page
    }
  })
end

四、错误处理 #

4.1 错误响应格式 #

elixir
defmodule HelloWeb.Api.V1.FallbackController do
  use HelloWeb, :controller

  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> json(%{errors: format_errors(changeset)})
  end

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> json(%{error: "Resource not found"})
  end

  def call(conn, {:error, :unauthorized}) do
    conn
    |> put_status(:unauthorized)
    |> json(%{error: "Unauthorized"})
  end

  defp format_errors(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
      Enum.reduce(opts, msg, fn {key, value}, acc ->
        String.replace(acc, "%{#{key}}", to_string(value))
      end)
    end)
  end
end

4.2 使用Fallback #

elixir
defmodule HelloWeb.Api.V1.UserController do
  use HelloWeb, :controller

  action_fallback HelloWeb.Api.V1.FallbackController

  def show(conn, %{"id" => id}) do
    with {:ok, user} <- get_user(id) do
      json(conn, %{data: user})
    end
  end

  defp get_user(id) do
    case Hello.Accounts.get_user(id) do
      nil -> {:error, :not_found}
      user -> {:ok, user}
    end
  end
end

五、API认证 #

5.1 Token认证 #

elixir
defmodule HelloWeb.ApiAuth do
  import Plug.Conn
  import Phoenix.Controller

  def init(opts), do: opts

  def call(conn, _opts) do
    with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
         {:ok, user} <- verify_token(token) do
      assign(conn, :current_user, user)
    else
      _ ->
        conn
        |> put_status(:unauthorized)
        |> json(%{error: "Unauthorized"})
        |> halt()
    end
  end

  defp verify_token(token) do
    case Hello.Accounts.get_user_by_api_token(token) do
      nil -> {:error, :invalid_token}
      user -> {:ok, user}
    end
  end
end

5.2 登录接口 #

elixir
defmodule HelloWeb.Api.V1.SessionController do
  use HelloWeb, :controller

  def create(conn, %{"email" => email, "password" => password}) do
    case Hello.Accounts.get_user_by_email_and_password(email, password) do
      nil ->
        conn
        |> put_status(:unauthorized)
        |> json(%{error: "Invalid credentials"})

      user ->
        token = Hello.Accounts.generate_api_token(user)
        json(conn, %{data: %{token: token, user: user}})
    end
  end
end

六、资源操作 #

6.1 CRUD操作 #

elixir
defmodule HelloWeb.Api.V1.PostController do
  use HelloWeb, :controller

  action_fallback HelloWeb.Api.V1.FallbackController

  def index(conn, _params) do
    posts = Hello.Blog.list_posts()
    json(conn, %{data: posts})
  end

  def show(conn, %{"id" => id}) do
    with {:ok, post} <- get_post(id) do
      json(conn, %{data: post})
    end
  end

  def create(conn, %{"post" => post_params}) do
    user = conn.assigns.current_user

    with {:ok, post} <- Hello.Blog.create_post(user.id, post_params) do
      conn
      |> put_status(:created)
      |> json(%{data: post})
    end
  end

  def update(conn, %{"id" => id, "post" => post_params}) do
    with {:ok, post} <- get_post(id),
         {:ok, updated_post} <- Hello.Blog.update_post(post, post_params) do
      json(conn, %{data: updated_post})
    end
  end

  def delete(conn, %{"id" => id}) do
    with {:ok, post} <- get_post(id),
         {:ok, _} <- Hello.Blog.delete_post(post) do
      send_resp(conn, :no_content, "")
    end
  end

  defp get_post(id) do
    case Hello.Blog.get_post(id) do
      nil -> {:error, :not_found}
      post -> {:ok, post}
    end
  end
end

七、参数验证 #

7.1 使用Changeset验证 #

elixir
def create(conn, %{"post" => post_params}) do
  changeset = Post.changeset(%Post{}, post_params)

  if changeset.valid? do
    case Hello.Blog.create_post(post_params) do
      {:ok, post} ->
        conn
        |> put_status(:created)
        |> json(%{data: post})

      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{errors: format_errors(changeset)})
    end
  else
    conn
    |> put_status(:unprocessable_entity)
    |> json(%{errors: format_errors(changeset)})
  end
end

7.2 强参数 #

elixir
def create(conn, params) do
  post_params = Map.take(params["post"], ["title", "body", "status"])

  case Hello.Blog.create_post(post_params) do
    {:ok, post} ->
      json(conn, %{data: post})

    {:error, changeset} ->
      json(conn, %{errors: format_errors(changeset)})
  end
end

八、API文档 #

8.1 使用Swagger #

elixir
defmodule HelloWeb.Api.V1.UserController do
  use HelloWeb, :controller

  use PhoenixSwagger

  swagger_path :index do
    get "/api/v1/users"
    description "List all users"
    produces "application/json"
    response 200, "Success"
  end

  swagger_path :show do
    get "/api/v1/users/{id}"
    description "Get user by ID"
    produces "application/json"
    parameter :id, :path, :integer, "User ID", required: true
    response 200, "Success"
    response 404, "Not found"
  end

  def index(conn, _params) do
    users = Hello.Accounts.list_users()
    json(conn, %{data: users})
  end
end

九、测试API #

9.1 API测试 #

elixir
defmodule HelloWeb.Api.V1.UserControllerTest do
  use HelloWeb.ConnCase

  describe "index" do
    test "lists all users", %{conn: conn} do
      user = user_fixture()

      conn =
        conn
        |> get(~p"/api/v1/users")

      assert json_response(conn, 200)["data"] |> length() == 1
    end
  end

  describe "show" do
    test "shows user", %{conn: conn} do
      user = user_fixture()

      conn =
        conn
        |> get(~p"/api/v1/users/#{user.id}")

      assert json_response(conn, 200)["data"]["id"] == user.id
    end

    test "returns 404 for non-existent user", %{conn: conn} do
      conn =
        conn
        |> get(~p"/api/v1/users/999")

      assert json_response(conn, 404)["error"] == "Resource not found"
    end
  end
end

十、总结 #

10.1 核心概念 #

概念 说明
RESTful 资源设计
JSON 数据格式
Token API认证
版本控制 API版本管理
错误处理 统一错误格式

10.2 最佳实践 #

  • 使用RESTful设计
  • 统一响应格式
  • 完善的错误处理
  • API版本控制
  • 编写测试

10.3 下一步 #

现在你已经了解了API开发,接下来让我们学习 测试与部署,深入了解测试和生产部署!

最后更新:2026-03-28