Laravel Requests and Responses
Requests and responses are Laravel's "inputs and outputs"—data is extracted when a request comes in, and data is packaged when a response goes out; both processes require precise control.
1. What You'll Learn
- Request Object: input/query/has/only/except and Type Conversion
- File Upload Processing: store/storedAs/S3 Integration
- Response Builder: json/view/download/redirect/stream
- API Resource Pagination: LengthAwarePaginator and Cursor Pagination
- Standardization of Response Macros and Global Response Formats
2. A True Story from an API Developer
(1) Pain Point: Each endpoint returns data in a different format
Bob wrote 10 endpoints for the ShopMetrics API—some return {data: [...]}, some return {shops: [...]}, and others return arrays directly. When integrating the API into the front end, Alice had to write different parsing logic for each endpoint. To make matters worse, paginated data was returned as either total_pages or last_page, making it impossible to standardize the front-end pagination component.
(2) Methods for Solving Standardized Responses
Laravel's API Resource and response macros standardize the output format—all endpoints return the same JSON structure, and paginated data follows a consistent format.
// Standardized response — every endpoint follows this format
return ShopResource::collection($shops);
// {
// "data": [...],
// "meta": {"current_page": 1, "total": 50},
// "links": {"next": "...", "prev": "..."}
// }
(3) Revenue
After Alice adopted a unified format, the number of front-end parsing logic sets was reduced from 10 to 1, and the reuse rate of the pagination component reached 100%.
3. The Request Object
(1) Get Input
// Get single input
$name = $request->input('name');
$name = $request->input('name', 'default value');
// Get from query string only
$sort = $request->query('sort', 'created_at');
// Get multiple inputs
$filtered = $request->only(['name', 'email', 'status']);
$filtered = $request->except(['password', '_token']);
// Check existence
if ($request->has('search')) { ... }
if ($request->filled('search')) { ... } // has + not empty
if ($request->missing('search')) { ... }
// Type conversion
$page = $request->integer('page', 1);
$active = $request->boolean('active', false);
$date = $request->date('published_at', 'Y-m-d');
| Method | Description | Difference |
|---|---|---|
input() |
All inputs (query+body) | Most common |
query() |
Query string only | GET parameters |
post() |
POST body only | Form data |
only() |
Extract Specified Fields | Whitelist |
except() |
Exclude Specified Fields | Blacklist |
has() |
Field exists | Contains null values |
filled() |
Field exists and is not empty | Exclude null values |
(1) ▶ Example: Processing ShopMetrics API Requests
// app/Http/Controllers/Api/ShopController.php
public function index(Request $request): JsonResponse
{
$query = Shop::where('tenant_id', tenant()->id);
// Search filter
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->search}%");
}
// Status filter
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Sort
$sortField = $request->input('sort', 'created_at');
$sortDir = $request->input('direction', 'desc');
$query->orderBy($sortField, $sortDir);
// Paginate
$perPage = $request->integer('per_page', 15);
$shops = $query->paginate($perPage);
return ShopResource::collection($shops);
}
Output:
// Execution Successful
4. File Upload
(1) Basic Upload Processing
// Validate file upload
$validated = $request->validate([
'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
// Store file
$path = $request->file('logo')->store('shops/logos', 'public');
// => "shops/logos/abc123.jpg"
// Store with custom name
$path = $request->file('logo')->storeAs(
'shops/logos',
$shop->slug . '.' . $request->file('logo')->extension(),
'public',
);
// Get file info
$file = $request->file('logo');
$file->getClientOriginalName();
$file->getClientOriginalExtension();
$file->getSize(); // bytes
$file->getMimeType();
| Method | Description |
|---|---|
store() |
Save to specified directory with a random filename |
storeAs() |
Save to a specified directory with a custom filename |
storePublicly() |
Save to the public directory |
isValid() |
Check if the upload was successful |
(1) ▶ Example: Uploading a ShopMetrics Store Logo
// app/Http/Controllers/ShopLogoController.php
class ShopLogoController extends Controller
{
public function update(Request $request, Shop $shop): RedirectResponse
{
$validated = $request->validate([
'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
// Delete old logo if exists
if ($shop->logo_path) {
Storage::disk('public')->delete($shop->logo_path);
}
// Store new logo
$path = $request->file('logo')->store("shops/{$shop->id}/logos", 'public');
$shop->update(['logo_path' => $path]);
return back()->with('success', 'Logo updated.');
}
}
Output:
// Execution Successful
5. Response Builder
(1) Response Type
// JSON response
return response()->json(['message' => 'Created', 'data' => $shop], 201);
// View response
return response()->view('shops.show', compact('shop'), 200);
// Redirect
return redirect()->route('shops.show', $shop);
return back()->withInput()->withErrors($errors);
return redirect()->away('https://external-site.com');
// File download
return response()->download(storage_path('app/reports/report.pdf'));
return response()->streamDownload(function () {
echo generateCsv();
}, 'orders.csv', ['Content-Type' => 'text/csv']);
// No content
return response()->noContent(); // 204
| Response Type | Method | HTTP Status Code |
|---|---|---|
| JSON | response()->json() |
200/201/422 |
| View | response()->view() |
200 |
| Redirect | redirect() |
302 |
| Download | response()->download() |
200 |
| Stream Download | response()->streamDownload() |
200 |
| No content | response()->noContent() |
204 |
(1) ▶ Example: ShopMetrics CSV Export Streaming Response
// app/Http/Controllers/ExportController.php
class ExportController extends Controller
{
public function exportOrders(Request $request, Shop $shop): StreamedResponse
{
$this->authorize('view', $shop);
return response()->streamDownload(function () use ($shop) {
$csv = fopen('php://output', 'w');
fputcsv($csv, ['Order ID', 'Customer', 'Total', 'Status', 'Date']);
$shop->orders()
->with('user')
->orderBy('created_at', 'desc')
->chunk(500, function ($orders) use ($csv) {
foreach ($orders as $order) {
fputcsv($csv, [
$order->order_number,
$order->user->name,
$order->total,
$order->status,
$order->created_at->format('Y-m-d'),
]);
}
});
fclose($csv);
}, "orders-{$shop->slug}-" . now()->format('Y-m-d') . '.csv', [
'Content-Type' => 'text/csv',
]);
}
}
Output:
// Execution Successful
6. API Pagination
(1) Comparison of Pagination Methods
| Method | Number of Queries | Returned Information | Use Cases |
|---|---|---|---|
paginate() |
2 (COUNT+SELECT) | total/last_page/links | Total number of pages required |
simplePaginate() |
1 (SELECT) | next/prev links | Total pages not required |
cursorPaginate() |
1 (SELECT+WHERE) | next/prev cursor | large dataset |
(2) Pagination in JSON Format
{
"data": [
{"id": 1, "name": "Alice Store"},
{"id": 2, "name": "Bob Electronics"}
],
"current_page": 1,
"per_page": 15,
"total": 50,
"last_page": 4,
"from": 1,
"to": 15,
"links": {
"first": "/api/v1/shops?page=1",
"last": "/api/v1/shops?page=4",
"next": "/api/v1/shops?page=2",
"prev": null
}
}
(3) Cursor Pagination
// More efficient for large datasets
$shops = Shop::cursorPaginate(15);
// Uses "cursor" parameter instead of "page"
// URL: /api/shops?cursor=eyJpZCI6MTV9
// Next page URL
$shops->nextPageUrl();
// /api/shops?cursor=eyJpZCI6MzB9
(1) ▶ Example: ShopMetrics API Pagination and Filtering
// app/Http/Controllers/Api/OrderController.php
public function index(Request $request): JsonResponse
{
$query = Order::where('tenant_id', tenant()->id)
->with(['shop', 'user', 'items.product']);
// Filters
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->filled('shop_id')) {
$query->where('shop_id', $request->shop_id);
}
if ($request->filled('date_from')) {
$query->where('created_at', '>=', $request->date('date_from'));
}
if ($request->filled('date_to')) {
$query->where('created_at', '<=', $request->date('date_to'));
}
// Sort
$query->orderBy(
$request->input('sort_by', 'created_at'),
$request->input('sort_dir', 'desc'),
);
// Choose pagination strategy
$perPage = $request->integer('per_page', 15);
$orders = $request->boolean('cursor', false)
? $query->cursorPaginate($perPage)
: $query->paginate($perPage);
return OrderResource::collection($orders);
}
Output:
// Execution Successful
7. Response Macros and Format Standardization
(1) Define a response macro
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Response::macro('apiSuccess', function (mixed $data, string $message = 'Success', int $status = 200) {
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
], $status);
});
Response::macro('apiError', function (string $message, int $status = 400, array $errors = []) {
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
], $status);
});
}
(2) Using Response Macros
// In controller
return response()->apiSuccess($shop, 'Shop created', 201);
return response()->apiError('Shop not found', 404);
return response()->apiError('Validation failed', 422, $validator->errors()->toArray());
(1) ▶ Example: ShopMetrics API Standard Response Format
// app/Traits/ApiResponse.php
trait ApiResponse
{
protected function success(mixed $data, string $message = 'Success', int $status = 200): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
'timestamp' => now()->toISOString(),
], $status);
}
protected function error(string $message, int $status = 400, array $errors = []): JsonResponse
{
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
'timestamp' => now()->toISOString(),
], $status);
}
protected function paginated($resource, string $message = 'Success'): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $resource->resolve(),
'meta' => [
'current_page' => $resource->resource->currentPage(),
'per_page' => $resource->resource->perPage(),
'total' => $resource->resource->total(),
'last_page' => $resource->resource->lastPage(),
],
'links' => [
'first' => $resource->resource->url(1),
'last' => $resource->resource->url($resource->resource->lastPage()),
'next' => $resource->resource->nextPageUrl(),
'prev' => $resource->resource->previousPageUrl(),
],
]);
}
}
Output:
// Execution Successful
8. Comprehensive Example: The Complete Process of Handling ShopMetrics API Requests
// ============================================
// Comprehensive: ShopMetrics API Request-Response
// Covers: input handling, file upload, pagination, response format
// ============================================
// app/Http/Controllers/Api/ProductController.php
class ProductController extends Controller
{
use ApiResponse;
public function index(Request $request): JsonResponse
{
$query = Product::where('shop_id', $request->shop_id)
->with('categories');
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->search}%");
}
if ($request->filled('category')) {
$query->whereHas('categories', fn ($q) => $q->where('slug', $request->category));
}
if ($request->filled('min_price')) {
$query->where('price', '>=', $request->float('min_price'));
}
if ($request->filled('max_price')) {
$query->where('price', '<=', $request->float('max_price'));
}
if ($request->filled('in_stock') && $request->boolean('in_stock')) {
$query->where('stock', '>', 0);
}
$query->orderBy(
$request->input('sort', 'created_at'),
$request->input('direction', 'desc'),
);
$products = $query->paginate($request->integer('per_page', 15));
return $this->paginated(ProductResource::collection($products));
}
public function store(StoreProductRequest $request): JsonResponse
{
$product = Product::create($request->validated());
if ($request->hasFile('image')) {
$path = $request->file('image')->store("products/{$product->id}", 's3');
$product->update(['image_path' => $path]);
}
return $this->success(new ProductResource($product), 'Product created', 201);
}
public function export(Request $request, Shop $shop): StreamedResponse
{
$this->authorize('view', $shop);
return response()->streamDownload(function () use ($shop) {
echo ProductExporter::toCsv($shop);
}, "products-{$shop->slug}.csv", ['Content-Type' => 'text/csv']);
}
}
❓ FAQ
input() and query()?input() retrieves data from all sources (query string + request body), while query() retrieves data only from the URL query string. It's clearer to use input() for POST form data and query() for GET parameters.cursorPaginate and paginate?paginate for datasets smaller than 100,000 (requires displaying the total number of pages); use cursorPaginate for datasets larger than 100,000 (no COUNT query required, better performance). Infinite scrolling UI is best suited for cursor.ApiResponse trait or response macro to encapsulate the success, error, and paginated methods. All controllers should use these standardized response methods to ensure consistency in field names, status codes, and pagination formats.download() or streamDownload() for downloading large files?download() to read them directly into memory; for large files, use streamDownload() for streaming, which keeps memory usage constant. streamDownload() must be used for scenarios such as CSV export.📖 Summary
- The Request object provides methods such as input, query, only, and except to retrieve input
- Use
store()orstoreAs()to save uploaded files to a specified location on disk - Response supports multiple types: json/view/redirect/download/stream
- There are three pagination strategies for the API: paginate, simplePaginate, and cursorPaginate
- Adhere to a unified API output format for macros to avoid inconsistencies in the format of each endpoint
- Use
streamDownloadfor streaming large file exports to prevent memory overflow
📝 Exercises
-
Basic Exercise (⭐): Implement the GET /api/v1/shops endpoint for the ShopMetrics API, supporting the search, status, sort, and direction filter parameters, and use paginate to return JSON in paginated chunks.
-
Advanced Exercise (⭐⭐): Create an
ApiResponsetrait that encapsulates thesuccess,error, andpaginatedmethods, and use it in all API controllers to ensure a consistent response format. -
Challenge (⭐⭐⭐): Implement an API for a paginated order list using a cursor (GET /api/v1/orders?cursor=xxx), and compare the query performance differences between
paginateandcursorPaginatewhen processing 100,000 records.



