
Laravel REST API Part 9: Create Order API
Published on Dec 13, 2024
In this installment of our Laravel REST API series, we’ll take our API development a step further by creating a POST API for managing orders. Previously, we built a product listing API with filters, tested it, refactored it, and added caching to it. Now, we’ll implement an example of an Order API to handle POST requests.
Plan for the Order API
Let’s define the requirements for the Order API. The API should handle requests and responses with the following structure:
Endpoint:
[POST] /api/orders
Request:
{ "products": [ { "id": 1, "quantity": 2 }, { "id": 2, "quantity": 1 } ] }
Response:
{ "message": "Order success", "data": { "id": 123, "total_price": 200, "user": { "id": 1, "name": "John Doe", "email": "john@example.com" }, "products": [ { "id": 1, "name": "Product A", "price": 100, "quantity": 2 }, { "id": 2, "name": "Product B", "price": 50, "quantity": 1 } ] } }
That's our initial imagination of how it would be the API.
Implementation Steps
Step 1: Create Necessary Files
Run the following Artisan commands to generate the required files:
php artisan make:controller OrderController php artisan make:request StoreOrderRequest php artisan make:resource OrderResource
These commands create:
- OrderController for handling the logic.
- StoreOrderRequest for validating incoming requests.
- OrderResource for structuring JSON responses.
Step 2: Define the Route
Add the following route to the routes/api.php file:
use App\Http\Controllers\OrderController; Route::post('orders', [OrderController::class, 'store'])->name('orders.store');
Step 3: Add Validation Rules
In the StoreOrderRequest file, define validation rules for the API request:
return [ 'products' => 'required|array', 'products.*.id' => 'required|exists:products,id', 'products.*.quantity' => 'required|integer|min:1', ];
Step 4: Write the Controller Logic
Open OrderController and add the following logic for handling orders:
<?php namespace App\Http\Controllers; use App\Http\Requests\StoreOrderRequest; use App\Http\Resources\OrderResource; use App\Models\Order; use App\Models\Product; use DB; class OrderController extends Controller { public function store(StoreOrderRequest $request) { $data = $request->validated(); try { DB::beginTransaction(); $orderPrice = 0; $products = []; foreach ($data['products'] as $productData) { $product = Product::find($productData['id']); $product->reduceStock($productData['quantity']); $orderPrice += $product->price * $productData['quantity']; $products[$product->id] = ['quantity' => $productData['quantity']]; } $order = Order::create([ 'user_id' => auth()->user()->id, 'total_price' => $orderPrice, 'status' => Order::STATUS['Placed'] ]); $order->products()->sync($products); DB::commit(); $order->load(['user', 'products']); return response()->json([ 'message' => 'Order success', 'data' => new OrderResource($order), ]); } catch (\Exception $ex) { DB::rollBack(); return response()->json([ 'message' => 'Order failed', 'error' => $ex->getMessage(), ]); } } }
In the Product model, create a reduceStock method to manage stock:
public function reduceStock(int $quantity): void { if ($this->stock < $quantity) { throw new \Exception('Out of stock for: ' . $this->name); } $this->stock -= $quantity; $this->save(); }
In this code:
- We apply validation first, if the code is not valid it will return validation errors.
- Then se use try & catch in case we have any step failier.
- We used DB transaction because the process of creating an order including updates in many tables, so we need to make sure all data is correct.
- We go through each product to calculate the total order price and check and reduce product stock
- Then we created the order and synced the products in the pivot table
- And finally we lazy eager loaded the necessary relations that we will use in the Resource.
The resource file could be something like this:
<?php namespace App\Http\Resources; use App\Models\Order; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class OrderResource extends JsonResource { /** * Transform the resource into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ "id" => $this->id, "total_price" => $this->total_price, "status" => Order::STATUS[$this->status], "user" => new UserResource($this->whenLoaded("user")), "products" => ProductResource::collection($this->whenLoaded("products")), ]; } }
Step 5: Refactor Using an Action Class
As mentioned in the introduction we introduced actions with products, so we will use the same pattern with orders as well.
So, let's extract some logic from the controller and Create Order/CreateOrderAction.php inside Actions folder, and we could have the following logic:
<?php namespace App\Http\Actions\Order; use App\Models\Order; use App\Models\Product; class CreateOrderAction { public function execute(array $data): Order { $orderPrice = 0; $products = []; foreach ($data['products'] as $productData) { $product = Product::find($productData['id']); $product->reduceStock($productData['quantity']); $orderPrice += $product->price * $productData['quantity']; $products[$product->id] = ['quantity' => $productData['quantity']]; } $order = Order::create([ 'user_id' => auth()->user()->id, 'total_price' => $orderPrice, 'status' => Order::STATUS['Placed'] ]); $order->products()->sync($products); return $order; } }
Update the Controller
Modify the store method in OrderController to use the CreateOrderAction class:
public function store(StoreOrderRequest $request, CreateOrderAction $createOrderAction) { $data = $request->validated(); try { DB::beginTransaction(); $order = $createOrderAction->execute($data); DB::commit(); $order->load(['user', 'products']); return response()->json([ 'message' => 'Order success', 'data' => new OrderResource($order), ]); } catch (\Exception $ex) { DB::rollBack(); return response()->json([ 'message' => 'Order failed', 'error' => $ex->getMessage(), ]); } }
Testing the Order API
I want to include also testing example of a POST API and I don't want to make this series very long, so I'll just add the logic here:
Step 1: Create a Test Class
Run the following command to generate a test class:
php artisan make:test OrderControllerTest
If you have a lot of logic inside OrderController you may make a separate file for each API, but for now we are good!
Step 2: Set Up the Test
Use the RefreshDatabase trait and mock authentication:
public function setUp(): void { parent::setUp(); $this->actingAs(User::factory()->create()); }
Step 3: Validate Inputs
Let's start our POST API with testing validations and inputs, we might have something similar to that:
public function test_it_requires_products_array() { $response = $this->postJson('/api/orders', []); $response->assertStatus(422)->assertJsonValidationErrors(['products']); } public function test_it_requires_valid_product_id_and_quantity() { $response = $this->postJson('/api/orders', [ 'products' => [ ['id' => 999, 'quantity' => 1], ['id' => 1, 'quantity' => 0], ] ]); $response->assertStatus(422); $response->assertJsonValidationErrors(['products.0.id', 'products.1.quantity']); }
Those are two basic test cases to make sure that there is a validation error returns when user doesn't send a products array or send non exist products
Step 4: Test Order Creation (The happy path)
public function test_it_creates_order_successfully_with_valid_data() { $product = Product::factory()->create(['price' => 100, 'stock' => 10]); $response = $this->postJson('/api/orders', [ 'products' => [ ['id' => $product->id, 'quantity' => 2] ] ]); $response->assertStatus(200); $this->assertDatabaseHas('orders', [ 'user_id' => auth()->id(), 'total_price' => 200, ]); }
This test case creating a product and trying to make an order with that product, and we have three assertions
First one: we make sure that we receive success status and we have the exact Json response as we excpected.
Second one: we make sure that the order is inserted in the database with correct data
Third one: we do a double check on the product quantity as it should be reduced
Step 5: Test Stock Exception
In Product model in reduceStock function we fired an exception in case the user sends quantity bigger than we have in the stock. So, we may want to test that as well:
public function test_it_does_not_create_order_on_exception() { $product = Product::factory()->create(['stock' => 10]); $response = $this->postJson('/api/orders', [ 'products' => [ ['id' => $product->id, 'quantity' => 11] ] ]); $response->assertStatus(200)->assertJson(['message' => 'Order failed']); }
Here we make sure that the Order didn't created and the product stock still the same
Conclusion
In this article, we explored the creation of a POST API for orders in Laravel, including validation, logic implementation, refactoring, and testing. This approach ensures a robust and scalable API for handling orders and similar POST requests. In the real world you might have more logic like sending a notifications, and payment of-course, we only implemented the basic stuff and you may take it further from here.