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