REST API Development
Building a Complete RESTful API - Part 3
Building a Complete RESTful API - Part 3
In this final part, we'll add comprehensive testing, generate interactive API documentation with Swagger/OpenAPI, deploy to production, and review all best practices we've learned throughout this course.
API Testing with PHPUnit
Testing is crucial for API reliability. Let's write comprehensive tests:
tests/Feature/Api/AuthTest.php:
<?php
namespace Tests\Feature\Api;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AuthTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function user_can_register()
{
$response = $this->postJson('/api/v1/auth/register', [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
$response->assertStatus(201)
->assertJsonStructure([
'access_token',
'token_type',
'expires_in',
'user' => ['id', 'name', 'email'],
]);
$this->assertDatabaseHas('users', [
'email' => 'john@example.com',
]);
}
/** @test */
public function user_cannot_register_with_invalid_data()
{
$response = $this->postJson('/api/v1/auth/register', [
'name' => '',
'email' => 'invalid-email',
'password' => 'short',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['name', 'email', 'password']);
}
/** @test */
public function user_can_login_with_valid_credentials()
{
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);
$response = $this->postJson('/api/v1/auth/login', [
'email' => 'test@example.com',
'password' => 'password123',
]);
$response->assertStatus(200)
->assertJsonStructure([
'access_token',
'token_type',
'expires_in',
'user',
]);
}
/** @test */
public function user_cannot_login_with_invalid_credentials()
{
$response = $this->postJson('/api/v1/auth/login', [
'email' => 'test@example.com',
'password' => 'wrong-password',
]);
$response->assertStatus(401)
->assertJson(['message' => 'Invalid credentials']);
}
/** @test */
public function authenticated_user_can_get_profile()
{
$user = User::factory()->create();
$response = $this->actingAs($user, 'api')
->getJson('/api/v1/auth/me');
$response->assertStatus(200)
->assertJson([
'data' => [
'id' => $user->id,
'email' => $user->email,
],
]);
}
/** @test */
public function unauthenticated_user_cannot_access_protected_routes()
{
$response = $this->getJson('/api/v1/auth/me');
$response->assertStatus(401);
}
}
tests/Feature/Api/ProductTest.php:
<?php
namespace Tests\Feature\Api;
use App\Models\Category;
use App\Models\Product;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class ProductTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Storage::fake('public');
}
/** @test */
public function guest_can_view_products()
{
Product::factory()->count(5)->create();
$response = $this->getJson('/api/v1/products');
$response->assertStatus(200)
->assertJsonStructure([
'data' => [
'*' => ['id', 'name', 'price', 'slug'],
],
'links',
'meta',
]);
}
/** @test */
public function guest_can_view_single_product()
{
$product = Product::factory()->create();
$response = $this->getJson("/api/v1/products/{$product->id}");
$response->assertStatus(200)
->assertJson([
'data' => [
'id' => $product->id,
'name' => $product->name,
],
]);
}
/** @test */
public function admin_can_create_product()
{
$admin = User::factory()->create(['role' => 'admin']);
$category = Category::factory()->create();
$response = $this->actingAs($admin, 'api')
->postJson('/api/v1/products', [
'category_id' => $category->id,
'name' => 'Test Product',
'slug' => 'test-product',
'description' => 'Test description',
'price' => 99.99,
'sku' => 'TEST-001',
'stock_quantity' => 10,
]);
$response->assertStatus(201)
->assertJson([
'data' => ['name' => 'Test Product'],
]);
$this->assertDatabaseHas('products', [
'slug' => 'test-product',
]);
}
/** @test */
public function admin_can_upload_product_images()
{
$admin = User::factory()->create(['role' => 'admin']);
$category = Category::factory()->create();
$response = $this->actingAs($admin, 'api')
->postJson('/api/v1/products', [
'category_id' => $category->id,
'name' => 'Test Product',
'slug' => 'test-product',
'price' => 99.99,
'sku' => 'TEST-001',
'stock_quantity' => 10,
'images' => [
UploadedFile::fake()->image('product1.jpg'),
UploadedFile::fake()->image('product2.jpg'),
],
]);
$response->assertStatus(201);
$product = Product::where('slug', 'test-product')->first();
$this->assertCount(2, $product->images);
}
/** @test */
public function regular_user_cannot_create_product()
{
$user = User::factory()->create(['role' => 'user']);
$category = Category::factory()->create();
$response = $this->actingAs($user, 'api')
->postJson('/api/v1/products', [
'category_id' => $category->id,
'name' => 'Test Product',
'slug' => 'test-product',
'price' => 99.99,
'sku' => 'TEST-001',
'stock_quantity' => 10,
]);
$response->assertStatus(403);
}
/** @test */
public function products_can_be_filtered_by_category()
{
$category1 = Category::factory()->create();
$category2 = Category::factory()->create();
Product::factory()->count(3)->create(['category_id' => $category1->id]);
Product::factory()->count(2)->create(['category_id' => $category2->id]);
$response = $this->getJson("/api/v1/products?category_id={$category1->id}");
$response->assertStatus(200)
->assertJsonCount(3, 'data');
}
/** @test */
public function products_can_be_searched()
{
Product::factory()->create(['name' => 'Blue Laptop']);
Product::factory()->create(['name' => 'Red Phone']);
Product::factory()->create(['name' => 'Blue Mouse']);
$response = $this->getJson('/api/v1/products?search=blue');
$response->assertStatus(200)
->assertJsonCount(2, 'data');
}
}
Testing Best Practices:
- Use RefreshDatabase trait to reset database between tests
- Test both success and failure scenarios
- Test authentication and authorization
- Test validation rules
- Test filters, sorting, and pagination
- Aim for 80%+ code coverage
API Documentation with Swagger/OpenAPI
Generate interactive API documentation:
Install L5-Swagger:
composer require darkaonline/l5-swagger
php artisan vendor:publish --provider="L5Swagger\L5SwaggerServiceProvider"
php artisan l5-swagger:generate
app/Http/Controllers/Api/Controller.php - OpenAPI Annotations:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller as BaseController;
/**
* @OA\Info(
* title="E-Commerce API",
* version="1.0.0",
* description="RESTful API for e-commerce platform",
* @OA\Contact(
* email="support@example.com"
* ),
* @OA\License(
* name="MIT",
* url="https://opensource.org/licenses/MIT"
* )
* )
*
* @OA\Server(
* url="http://localhost:8000",
* description="Local Development Server"
* )
*
* @OA\Server(
* url="https://api.example.com",
* description="Production Server"
* )
*
* @OA\SecurityScheme(
* securityScheme="bearerAuth",
* type="http",
* scheme="bearer",
* bearerFormat="JWT"
* )
*/
class Controller extends BaseController
{
//
}
ProductController Documentation Example:
<?php
/**
* @OA\Get(
* path="/api/v1/products",
* summary="Get list of products",
* tags={"Products"},
* @OA\Parameter(
* name="category_id",
* in="query",
* description="Filter by category ID",
* required=false,
* @OA\Schema(type="integer")
* ),
* @OA\Parameter(
* name="search",
* in="query",
* description="Search in product name and description",
* required=false,
* @OA\Schema(type="string")
* ),
* @OA\Parameter(
* name="min_price",
* in="query",
* description="Minimum price filter",
* required=false,
* @OA\Schema(type="number")
* ),
* @OA\Parameter(
* name="per_page",
* in="query",
* description="Items per page",
* required=false,
* @OA\Schema(type="integer", default=15)
* ),
* @OA\Response(
* response=200,
* description="Successful operation",
* @OA\JsonContent(
* @OA\Property(property="data", type="array",
* @OA\Items(ref="#/components/schemas/Product")
* ),
* @OA\Property(property="links", type="object"),
* @OA\Property(property="meta", type="object")
* )
* )
* )
*/
public function index(Request $request): AnonymousResourceCollection
{
// ...
}
/**
* @OA\Post(
* path="/api/v1/products",
* summary="Create a new product",
* tags={"Products"},
* security={{"bearerAuth":{}}},
* @OA\RequestBody(
* required=true,
* @OA\MediaType(
* mediaType="multipart/form-data",
* @OA\Schema(
* required={"category_id","name","slug","price","sku","stock_quantity"},
* @OA\Property(property="category_id", type="integer"),
* @OA\Property(property="name", type="string"),
* @OA\Property(property="slug", type="string"),
* @OA\Property(property="description", type="string"),
* @OA\Property(property="price", type="number", format="float"),
* @OA\Property(property="sale_price", type="number", format="float"),
* @OA\Property(property="sku", type="string"),
* @OA\Property(property="stock_quantity", type="integer"),
* @OA\Property(
* property="images[]",
* type="array",
* @OA\Items(type="string", format="binary")
* )
* )
* )
* ),
* @OA\Response(
* response=201,
* description="Product created successfully",
* @OA\JsonContent(ref="#/components/schemas/Product")
* ),
* @OA\Response(response=401, description="Unauthenticated"),
* @OA\Response(response=403, description="Forbidden"),
* @OA\Response(response=422, description="Validation error")
* )
*/
public function store(StoreProductRequest $request): JsonResponse
{
// ...
}
Documentation Access: After generating, access your API documentation at
http://localhost:8000/api/documentation. Your team and API consumers can explore and test endpoints interactively.
Performance Optimization
Query Optimization:
<?php
// ❌ N+1 Query Problem
$products = Product::all();
foreach ($products as $product) {
echo $product->category->name; // Queries category for each product
}
// ✅ Eager Loading
$products = Product::with('category')->get();
foreach ($products as $product) {
echo $product->category->name; // No additional queries
}
// ✅ Lazy Eager Loading
$products = Product::all();
$products->load('category', 'images');
// ✅ Counting Related Models
$products = Product::withCount('reviews')->get();
foreach ($products as $product) {
echo $product->reviews_count; // No query
}
Response Caching:
<?php
use Illuminate\Support\Facades\Cache;
public function getFeaturedProducts(): JsonResponse
{
$products = Cache::remember('featured_products', 3600, function () {
return Product::with(['category', 'images'])
->featured()
->active()
->limit(10)
->get();
});
return response()->json([
'data' => ProductResource::collection($products),
]);
}
// Clear cache when products are updated
public function update(UpdateProductRequest $request, Product $product): ProductResource
{
$this->productRepository->update($product, $request->validated());
// Clear relevant caches
Cache::forget('featured_products');
Cache::tags(['products'])->flush();
return new ProductResource($product->fresh());
}
API Best Practices Summary
Complete API Best Practices Checklist:
1. API Design
- ✓ Use RESTful conventions (GET, POST, PUT, DELETE)
- ✓ Version your API (/api/v1/)
- ✓ Use plural nouns for resources (/products, not /product)
- ✓ Use proper HTTP status codes
- ✓ Return consistent response structures
- ✓ Implement HATEOAS for navigation links
2. Security
- ✓ Use JWT or OAuth2 for authentication
- ✓ Implement rate limiting
- ✓ Validate all inputs
- ✓ Sanitize outputs to prevent XSS
- ✓ Use HTTPS in production
- ✓ Implement CORS properly
- ✓ Hash passwords with bcrypt/argon2
- ✓ Use parameterized queries (prevent SQL injection)
3. Performance
- ✓ Implement pagination for list endpoints
- ✓ Use eager loading to avoid N+1 queries
- ✓ Cache frequently accessed data
- ✓ Add database indexes on foreign keys
- ✓ Optimize images before storing
- ✓ Use queue jobs for heavy operations
- ✓ Enable gzip compression
4. Documentation
- ✓ Use OpenAPI/Swagger for interactive docs
- ✓ Document all endpoints with examples
- ✓ Provide authentication instructions
- ✓ Include error response examples
- ✓ Keep documentation up to date
5. Testing
- ✓ Write feature tests for all endpoints
- ✓ Test authentication and authorization
- ✓ Test validation rules
- ✓ Test error handling
- ✓ Aim for 80%+ code coverage
- ✓ Use factories for test data
6. Error Handling
- ✓ Return meaningful error messages
- ✓ Use consistent error format
- ✓ Log errors for debugging
- ✓ Never expose sensitive data in errors
- ✓ Handle exceptions globally
7. Deployment
- ✓ Use environment variables for config
- ✓ Enable production error logging
- ✓ Set up monitoring and alerts
- ✓ Implement health check endpoints
- ✓ Use CI/CD for automated deployments
- ✓ Keep dependencies up to date
Common API Patterns Quick Reference
Filtering:
GET /api/v1/products?category_id=5&is_active=true
Sorting:
GET /api/v1/products?sort_by=price&sort_order=desc
Pagination:
GET /api/v1/products?page=2&per_page=20
Response:
{
"data": [...],
"links": {
"first": "...?page=1",
"last": "...?page=10",
"prev": "...?page=1",
"next": "...?page=3"
},
"meta": {
"current_page": 2,
"last_page": 10,
"per_page": 20,
"total": 200
}
}
Search:
GET /api/v1/products?search=laptop
Field Selection (Sparse Fieldsets):
GET /api/v1/products?fields=id,name,price
Including Relationships:
GET /api/v1/products?include=category,reviews
Production Checklist: Before deploying to production, ensure: debug mode is OFF, all secrets are in environment variables, error logging is configured, HTTPS is enforced, rate limiting is enabled, backups are scheduled, monitoring is active, and documentation is updated.
Congratulations!
You've completed the REST API Development course! You've learned:
- ✓ RESTful API principles and design
- ✓ HTTP methods, status codes, and headers
- ✓ Authentication with JWT
- ✓ Validation and error handling
- ✓ Pagination, filtering, and sorting
- ✓ API versioning strategies
- ✓ Rate limiting and throttling
- ✓ File uploads and media handling
- ✓ API testing strategies
- ✓ Documentation with Swagger/OpenAPI
- ✓ Performance optimization
- ✓ Security best practices
- ✓ Deployment and DevOps
- ✓ Design patterns (Repository, DTO, Actions)
- ✓ Building a complete e-commerce API
Next Steps
Continue Your Learning:
- Build your own API projects to practice
- Explore GraphQL as an alternative to REST
- Learn about API gateways and microservices
- Study real-world APIs (Stripe, GitHub, Twitter)
- Contribute to open-source API projects
- Stay updated with API trends and best practices
Thank you for completing this course! You now have the skills to build professional, scalable RESTful APIs. Happy coding!