← Back to Developer Blog
💻 DeveloperMarch 2, 20268 min read

REST API Design: Stop Making It Complicated

Building a REST API? Here's how to design endpoints that make sense without overthinking it.

By Raspib Technology Team

REST API Design: Stop Making It Complicated

Seen APIs like this?

GET /getUserById?id=123
POST /createNewOrder
GET /get-all-products-list

Painful.

Here's how to design APIs that don't make developers cry.

Resource Naming

Use Nouns, Not Verbs

❌ /getUsers
❌ /createOrder
❌ /deleteProduct

✅ GET /users
✅ POST /orders
✅ DELETE /products/{id}

The HTTP method is the verb. The URL is the noun.

Plural Names

❌ /user
❌ /order
❌ /product

✅ /users
✅ /orders
✅ /products

Consistency matters. Always plural.

Nested Resources

✅ GET /users/123/orders
✅ GET /orders/456/items
✅ POST /products/789/reviews

Shows relationship clearly.

HTTP Methods

Use them correctly:

// GET: Retrieve data (no side effects)
GET /products
GET /products/123

// POST: Create new resource
POST /products

// PUT: Replace entire resource
PUT /products/123

// PATCH: Update partial resource
PATCH /products/123

// DELETE: Remove resource
DELETE /products/123

Status Codes That Matter

Success Codes

// 200 OK: Request succeeded
return response()->json($data, 200);

// 201 Created: Resource created
return response()->json($order, 201);

// 204 No Content: Success, no data to return
return response()->noContent();

Client Error Codes

// 400 Bad Request: Invalid data
return response()->json(['error' => 'Invalid input'], 400);

// 401 Unauthorized: Not authenticated
return response()->json(['error' => 'Login required'], 401);

// 403 Forbidden: Authenticated but no permission
return response()->json(['error' => 'Access denied'], 403);

// 404 Not Found: Resource doesn't exist
return response()->json(['error' => 'Product not found'], 404);

// 422 Unprocessable Entity: Validation failed
return response()->json(['errors' => $validator->errors()], 422);

Server Error Codes

// 500 Internal Server Error: Something broke
return response()->json(['error' => 'Server error'], 500);

// 503 Service Unavailable: Maintenance mode
return response()->json(['error' => 'Under maintenance'], 503);

Response Format

Consistent Structure

// Success response
{
    "success": true,
    "data": {
        "id": 123,
        "name": "Product Name"
    }
}

// Error response
{
    "success": false,
    "error": {
        "message": "Product not found",
        "code": "PRODUCT_NOT_FOUND"
    }
}

// Validation error
{
    "success": false,
    "errors": {
        "email": ["Email is required"],
        "password": ["Password must be at least 8 characters"]
    }
}

Pagination

{
    "data": [...],
    "meta": {
        "current_page": 1,
        "last_page": 10,
        "per_page": 20,
        "total": 200
    },
    "links": {
        "first": "/api/products?page=1",
        "last": "/api/products?page=10",
        "prev": null,
        "next": "/api/products?page=2"
    }
}

Laravel does this automatically:

return Product::paginate(20);

Filtering and Sorting

// GET /products?category=electronics&sort=-price&limit=20

public function index(Request $request)
{
    $query = Product::query();
    
    // Filter by category
    if ($request->has('category')) {
        $query->where('category', $request->category);
    }
    
    // Sort
    $sortField = ltrim($request->get('sort', 'id'), '-');
    $sortDirection = str_starts_with($request->sort, '-') ? 'desc' : 'asc';
    $query->orderBy($sortField, $sortDirection);
    
    // Limit
    $limit = min($request->get('limit', 20), 100);
    
    return $query->paginate($limit);
}

Versioning

URL Versioning (Simplest)

Route::prefix('v1')->group(function () {
    Route::get('/products', [ProductController::class, 'index']);
});

Route::prefix('v2')->group(function () {
    Route::get('/products', [ProductControllerV2::class, 'index']);
});
GET /api/v1/products
GET /api/v2/products

Clear. Easy to maintain.

Header Versioning

// Accept: application/vnd.api.v1+json

if ($request->header('Accept') === 'application/vnd.api.v2+json') {
    // Use v2 logic
}

Cleaner URLs, but more complex.

Our take: Use URL versioning. Simpler for clients.

Real API Example

Client: School management system API

Endpoints:

// Students
GET    /api/v1/students
GET    /api/v1/students/{id}
POST   /api/v1/students
PATCH  /api/v1/students/{id}
DELETE /api/v1/students/{id}

// Student's courses
GET    /api/v1/students/{id}/courses
POST   /api/v1/students/{id}/courses

// Courses
GET    /api/v1/courses
GET    /api/v1/courses/{id}
POST   /api/v1/courses
PATCH  /api/v1/courses/{id}
DELETE /api/v1/courses/{id}

Clean. Predictable. Easy to understand.

Error Handling

// app/Exceptions/Handler.php
public function render($request, Throwable $exception)
{
    if ($request->is('api/*')) {
        if ($exception instanceof ModelNotFoundException) {
            return response()->json([
                'success' => false,
                'error' => [
                    'message' => 'Resource not found',
                    'code' => 'RESOURCE_NOT_FOUND'
                ]
            ], 404);
        }
        
        if ($exception instanceof ValidationException) {
            return response()->json([
                'success' => false,
                'errors' => $exception->errors()
            ], 422);
        }
        
        // Generic error
        return response()->json([
            'success' => false,
            'error' => [
                'message' => 'Internal server error',
                'code' => 'SERVER_ERROR'
            ]
        ], 500);
    }
    
    return parent::render($request, $exception);
}

Documentation

Use OpenAPI/Swagger:

/**
 * @OA\Get(
 *     path="/api/products",
 *     summary="Get all products",
 *     @OA\Parameter(
 *         name="category",
 *         in="query",
 *         description="Filter by category"
 *     ),
 *     @OA\Response(response="200", description="Success")
 * )
 */
public function index(Request $request)
{
    // Implementation
}

Generates interactive documentation automatically.

Security

1. Always Validate

$request->validate([
    'email' => 'required|email',
    'password' => 'required|min:8',
]);

2. Rate Limiting

Route::middleware('throttle:60,1')->group(function () {
    // API routes
});

3. Authentication

Route::middleware('auth:sanctum')->group(function () {
    // Protected routes
});

Common Mistakes

Mistake 1: Inconsistent Naming

/users
/getOrders
/product-list

Pick one style. Stick to it.

Mistake 2: Wrong Status Codes

// Wrong: Returns 200 for errors
return response()->json(['error' => 'Not found'], 200);

// Right: Use proper status code
return response()->json(['error' => 'Not found'], 404);

Mistake 3: No Versioning

// Breaking change affects all clients
Route::get('/products', function () {
    // Changed response format
});

Always version from day one.

Bottom Line

Good API design is about consistency and predictability.

Use standard HTTP methods. Return proper status codes. Version your API.

Make it easy for developers to use. They'll thank you.


Building APIs?

We design and build REST APIs for Nigerian businesses. Clean, documented, scalable.

📞 WhatsApp: +234 708 711 0468
📧 info@raspibtech.com
📍 Lagos Island

Related:

Need Help with Your Project?

Let's discuss how Raspib Technology can help transform your business

Related Articles

REST API Design Best Practices - Simple Guide