Laravel RESTful API 开发

小飞兽 Laravel 7 次阅读 2026-07-29

本文介绍如何使用 Laravel 构建标准的 RESTful API。Laravel 提供了完整的 API 开发工具链:资源路由、API 资源类、认证系统(Sanctum/Passport)、频率限制等。

API 路由配置

// routes/api.php(默认已配置 prefix=api、throttle)
use App\Http\Controllers\Api\ArticleController;
use App\Http\Controllers\Api\AuthController;

Route::prefix("v1")->group(function () {
// 公开接口
Route::get("articles", [ArticleController::class, "index"]);
Route::get("articles/{article}", [ArticleController::class, "show"]);

// 需要认证的接口
Route::middleware("auth:sanctum")->group(function () {
Route::post("articles", [ArticleController::class, "store"]);
Route::put("articles/{article}", [ArticleController::class, "update"]);
Route::delete("articles/{article}", [ArticleController::class, "destroy"]);
Route::post("articles/{article}/like", [ArticleController::class, "like"]);
});
});

// API 资源路由(快速生成 CRUD)
Route::apiResource("articles", ArticleController::class);
// 等价于上面 5 条手动路由
// 仅部分方法:Route::apiResource("articles", ArticleController::class)->only(["index", "show"]);
// 排除部分:Route::apiResource("articles", ArticleController::class)->except(["destroy"]);

API Resource(资源转换)

// 创建资源类
php artisan make:resource ArticleResource
php artisan make:resource ArticleCollection

// app/Http/Resources/ArticleResource.php
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ArticleResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
"id" => $this->id,
"title" => $this->title,
"slug" => $this->slug,
"content" => $this->when($request->routeIs("api.v1.articles.show"), $this->content),
// content 只在详情页返回,列表不返回,减少数据传输
"author" => new UserResource($this->whenLoaded("author")),
"category" => new CategoryResource($this->whenLoaded("category")),
"tags" => TagResource::collection($this->whenLoaded("tags")),
"views" => $this->views,
"is_published" => $this->is_published,
"published_at" => $this->published_at?->toIso8601String(),
"created_at" => $this->created_at->toIso8601String(),
"updated_at" => $this->updated_at->toIso8601String(),
];
}
}

// app/Http/Resources/ArticleCollection.php
class ArticleCollection extends ResourceCollection
{
public $collects = ArticleResource::class;

public function toArray(Request $request): array
{
return [
"data" => $this->collection,
"meta" => [
"total" => $this->resource->total(),
"per_page" => $this->resource->perPage(),
"current_page" => $this->resource->currentPage(),
],
];
}
}

// app/Http/Resources/UserResource.php
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
"id" => $this->id,
"name" => $this->name,
"email" => $this->when($this->id === auth()->id(), $this->email),
"avatar" => $this->avatar ? url($this->avatar) : null,
];
}
}

控制器实现

// app/Http/Controllers/Api/ArticleController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreArticleRequest;
use App\Http\Requests\UpdateArticleRequest;
use App\Http\Resources\ArticleResource;
use App\Http\Resources\ArticleCollection;
use App\Models\Article;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class ArticleController extends Controller
{
public function index(Request $request): ArticleCollection
{
$perPage = min($request->input("per_page", 20), 100);

$articles = Article::with(["author:id,name,avatar", "category:id,name"])
->when($request->category_id, fn($q) => $q->where("category_id", $request->category_id))
->when($request->search, fn($q) => $q->where("title", "like", "%{$request->search}%"))
->orderBy($request->input("sort", "created_at"), $request->input("order", "desc"))
->paginate($perPage);

return new ArticleCollection($articles);
}

public function show(Request $request, Article $article): JsonResponse
{
$article->loadMissing(["author:id,name,avatar", "category:id,name", "tags"]);
return response()->json([
"success" => true,
"data" => new ArticleResource($article),
]);
}

public function store(StoreArticleRequest $request): JsonResponse
{
$data = $request->validated();
$tags = $data["tags"] ?? [];
unset($data["tags"]);

$article = Article::create(array_merge($data, [
"user_id" => $request->user()->id,
]));

if (!empty($tags)) {
$article->tags()->attach($tags);
}

$article->load(["author:id,name,avatar", "category:id,name", "tags"]);

return response()->json([
"success" => true,
"message" => "文章创建成功",
"data" => new ArticleResource($article),
], 201);
}

public function update(UpdateArticleRequest $request, Article $article): JsonResponse
{
$data = $request->validated();

if (isset($data["tags"])) {
$article->tags()->sync($data["tags"]);
unset($data["tags"]);
}

$article->update($data);

return response()->json([
"success" => true,
"message" => "文章更新成功",
"data" => new ArticleResource($article->fresh(["author", "category", "tags"])),
]);
}

public function destroy(Request $request, Article $article): JsonResponse
{
if ($request->user()->id !== $article->user_id) {
return response()->json(["success" => false, "message" => "无权删除"], 403);
}

$article->delete();

return response()->json(["success" => true, "message" => "文章已删除"]);
}

public function like(Request $request, Article $article): JsonResponse
{
$user = $request->user();
if ($article->likedBy($user)) {
$article->likes()->detach($user->id);
return response()->json(["success" => true, "liked" => false]);
}
$article->likes()->attach($user->id);
return response()->json(["success" => true, "liked" => true]);
}
}

API 认证(Sanctum)

// 安装 Sanctum
composer require laravel/sanctum
php artisan install:api

// User 模型加上 HasApiTokens trait
namespace App\Models;

use Laravel\Sanctum\HasApiTokens;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
use HasApiTokens, HasFactory;
}

// 认证路由
Route::prefix("v1")->group(function () {
Route::post("auth/register", [AuthController::class, "register"]);
Route::post("auth/login", [AuthController::class, "login"])->name("login");

Route::middleware("auth:sanctum")->group(function () {
Route::post("auth/logout", [AuthController::class, "logout"]);
Route::get("auth/me", [AuthController::class, "me"]);
});
});

// AuthController
class AuthController extends Controller
{
public function register(Request $request)
{
$validated = $request->validate([
"name" => "required|string|max:255",
"email" => "required|email|unique:users",
"password" => "required|string|min:6|confirmed",
]);

$user = User::create([
"name" => $validated["name"],
"email" => $validated["email"],
"password" => bcrypt($validated["password"]),
]);

$token = $user->createToken("api-token")->plainTextToken;

return response()->json([
"user" => new UserResource($user),
"token" => $token,
], 201);
}

public function login(Request $request)
{
if (!auth()->attempt($request->only("email", "password"))) {
return response()->json(["message" => "认证失败"], 401);
}

$user = auth()->user();
$token = $user->createToken("api-token")->plainTextToken;

return response()->json(["token" => $token]);
}

public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json(["message" => "已退出"]);
}

public function me(Request $request)
{
return response()->json(["user" => new UserResource($request->user())]);
}
}

// 客户端请求时带上 Token
// Authorization: Bearer 1|abc123...

API 错误处理

// app/Exceptions/Handler.php
namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class Handler extends ExceptionHandler
{
protected $dontFlash = ["current_password", "password", "password_confirmation"];

public function register(): void
{
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->expectsJson()) {
return response()->json([
"success" => false,
"message" => "资源不存在",
], 404);
}
});

$this->renderable(function (AuthenticationException $e, $request) {
if ($request->expectsJson()) {
return response()->json([
"success" => false,
"message" => "未认证,请登录",
], 401);
}
});
}

protected function invalidJson($request, ValidationException $exception)
{
return response()->json([
"success" => false,
"message" => "验证失败",
"errors" => $exception->errors(),
], $exception->status);
}
}

常见问题

    • Q: api.php 和 web.php 路由有什么区别?
      A:api.php 默认自动加 /api 前缀和 api 中间件组,且不支持 CSRF(因为 API 通常用 Token 认证)。web.php 不加前缀,需要 CSRF 保护。
    • Q: Sanctum 和 Passport 怎么选?
      A:Sanctum 适合 SPA(Vue/React)和移动端, token 存储在本地,简洁轻量;Passport 实现了完整的 OAuth2 协议,适合需要第三方授权(如「用 Google 登录」)的场景。
    • Q: API 返回数据时,为什么要在某些字段上用 when()?
      A:when() 是条件化属性,whenLoaded() 只在关系已预加载时才返回数据。这样列表接口只返回必要字段减少传输,详情接口再返回完整数据。
    • Q: 如何对 API 做版本管理?
      A:常用两种方式:URL 前缀(/api/v1/、/api/v2/)或 Header(Accept: application/vnd.api+json; version=2)。Laravel 官方推荐 URL 前缀,更直观且便于测试。

    延伸阅读

  • Laravel API 资源官方文档
  • Laravel API 性能优化:频率限制与缓存策略