Insights into Company
← Backend Concepts in Depth topics

REST APIs, GraphQL, cURL & Swagger

Designing REST APIs properly (resources, verbs, status codes, pagination, versioning), when GraphQL is a better fit, testing any API from the terminal with cURL, and documenting it with OpenAPI/Swagger, all with e-commerce, banking and payment endpoints in Laravel, Spring Boot and React.

Running examples: product catalogue, order placement, payment refunds, account statements

1. REST: resources, verbs and status codes

REST (Representational State Transfer) models an API as resources (nouns) addressed by URLs, manipulated with standard HTTP methods (verbs). The server is stateless: each request carries everything needed, including authentication. Responses are usually JSON.

Good REST design makes an API predictable: once a developer knows GET /orders, they can guess GET /orders/{id}, POST /orders and so on.

E-commerce order API
Method + URLMeaningSuccess codeIdempotent?
GET /api/v1/products?category=mobiles&sort=-price&page=2List with filter, sort, pagination200Yes
GET /api/v1/orders/ORD-187One order200 / 404Yes
POST /api/v1/ordersPlace an order201 + Location headerNo (use Idempotency-Key)
PATCH /api/v1/orders/ORD-187Change delivery address200Usually
POST /api/v1/orders/ORD-187/cancelDomain action (not CRUD)200 / 409 if shippedShould be
DELETE /api/v1/cart/items/55Remove from cart204Yes

Rules of thumb: plural nouns, no verbs in URLs except for domain actions, nested URLs only one level deep (/orders/187/items), version in the URL or a header (/v1), consistent JSON envelope, and never return a 200 with "error" in the body.

2. Laravel REST endpoint with resources and pagination

Controller, API Resource and cursor pagination

Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
    Route::apiResource('orders', OrderController::class)->only(['index', 'show', 'store']);
    Route::post('orders/{order}/cancel', CancelOrderController::class);
});

class OrderController extends Controller
{
    public function index(Request $request)
    {
        $orders = $request->user()->orders()
            ->with('items.product:id,name,price')            // avoid N+1
            ->when($request->status, fn ($q, $s) => $q->where('status', $s))
            ->latest('id')
            ->cursorPaginate(20);                           // fast on large tables, stable while new rows arrive
        return OrderResource::collection($orders);
    }

    public function store(StoreOrderRequest $request, OrderService $service)
    {
        $order = $service->place($request->user(), $request->validated());
        return (new OrderResource($order))->response()->setStatusCode(201)
            ->header('Location', route('orders.show', $order));
    }
}

class OrderResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->number,                          // public id, not the auto-increment
            'status' => $this->status,
            'total' => ['amount' => $this->total_paise / 100, 'currency' => 'INR'],
            'items' => OrderItemResource::collection($this->whenLoaded('items')),
            'placed_at' => $this->created_at->toIso8601String(),
        ];
    }
}

3. The same endpoint in Spring Boot

Controller with validation and a DTO

@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
class OrderController {
    private final OrderService service;

    @GetMapping
    Page<OrderDto> list(@AuthenticationPrincipal Jwt jwt, @RequestParam(required = false) String status, Pageable pageable) {
        return service.list(jwt.getSubject(), status, pageable).map(OrderDto::from);
    }

    @PostMapping
    ResponseEntity<OrderDto> place(@Valid @RequestBody PlaceOrderRequest body, @RequestHeader("Idempotency-Key") String key) {
        Order order = service.place(body, key);
        return ResponseEntity.created(URI.create("/api/v1/orders/" + order.getNumber())).body(OrderDto.from(order));
    }
}

record PlaceOrderRequest(@NotNull Long addressId, @NotEmpty @Size(max = 50) List<@Valid Line> items) {
    record Line(@NotNull Long productId, @Min(1) @Max(10) int quantity) {}
}

4. GraphQL: one endpoint, client chooses the fields

GraphQL exposes a single endpoint (POST /graphql) and a typed schema. The client sends a query describing exactly the fields it needs, possibly across several related objects, and gets back JSON in the same shape.

It solves two REST problems. Over-fetching: a mobile order list needs 4 fields but /orders returns 40. Under-fetching: one screen needs order + items + product images + delivery status, which is 4 REST calls. With GraphQL it is one request with only those fields.

Costs: caching by URL no longer works (everything is POST /graphql), a client can ask for very deep or expensive queries (limit depth and complexity), and N+1 queries appear in resolvers unless you batch them (DataLoader, Lighthouse @hasMany batching).

Schema and a query for an order-tracking screen

type Order { id: ID!  status: String!  total: Float!  items: [OrderItem!]!  shipment: Shipment }
type OrderItem { quantity: Int!  product: Product! }
type Product { name: String!  imageUrl: String }
type Query { order(id: ID!): Order }
type Mutation { cancelOrder(id: ID!, reason: String): Order }

query TrackOrder {
  order(id: "ORD-187") {
    status
    items { quantity product { name imageUrl } }
    shipment { courier trackingNumber eta }
  }
}

Laravel with Lighthouse (schema-first)

# graphql/schema.graphql
type Query {
  order(id: ID! @eq(key: "number")): Order @find @guard @can(ability: "view", find: "number")
}
type Order { number: ID!  status: String!  items: [OrderItem!]! @hasMany }

Spring for GraphQL

@Controller
class OrderGraphController {
    @QueryMapping
    Order order(@Argument String id) { return orders.findByNumber(id); }

    @BatchMapping                                   // loads items for many orders in one query (no N+1)
    Map<Order, List<OrderItem>> items(List<Order> orders) { return items.findByOrders(orders); }
}

React with Apollo Client

const TRACK = gql`query($id: ID!) { order(id: $id) { status shipment { courier eta } } }`;

function Tracking({ id }) {
  const { data, loading, error } = useQuery(TRACK, { variables: { id } });
  if (loading) return <Spinner />;
  if (error) return <p>Could not load tracking.</p>;
  return <p>{data.order.status} with {data.order.shipment?.courier}, arriving {data.order.shipment?.eta}</p>;
}
REST or GraphQL?
SituationPrefer
Public payment API used by thousands of merchantsREST (simple, cacheable, easy to document and version)
Mobile app screens combining many related objectsGraphQL
File upload, webhooks, simple CRUDREST
Many teams' services behind one client-facing APIGraphQL gateway / federation

5. cURL: calling and debugging APIs from the terminal

cURL is a command-line HTTP client available on every server. Engineers use it to test endpoints, reproduce bugs ("send me the curl"), check headers and TLS, and call webhooks. Postman and browser DevTools can export any request as a cURL command.

Everyday cURL commands

# GET with a token, show response headers (-i)
curl -i https://api.shop.test/api/v1/orders/ORD-187 -H "Authorization: Bearer 12|abc..." -H "Accept: application/json"

# POST JSON (place an order) with an idempotency key
curl -X POST https://api.shop.test/api/v1/orders \
  -H "Authorization: Bearer 12|abc..." -H "Content-Type: application/json" -H "Idempotency-Key: 7f9c-1" \
  -d '{"address_id": 3, "items": [{"product_id": 10, "quantity": 2}]}'

# Upload a file (multipart)
curl -X POST https://bank.test/api/kyc -H "Authorization: Bearer ..." -F "pan=@/home/me/pan.jpg"

# Debug: timing breakdown, verbose TLS handshake, follow redirects
curl -o /dev/null -s -w "dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} total=%{time_total}\n" https://bank.test
curl -v https://bank.test
curl -L http://bank.test          # follows the 301 to https

Calling a payment gateway from PHP (Laravel HTTP client is a cURL wrapper)

$response = Http::withBasicAuth(config('services.razorpay.key'), config('services.razorpay.secret'))
    ->timeout(10)->retry(2, 500, throw: false)
    ->post('https://api.razorpay.com/v1/payments/' . $paymentId . '/refund', ['amount' => 50000]);

if ($response->failed()) {
    throw new RefundFailedException($response->json('error.description'));
}

// Raw PHP cURL, what the wrapper does underneath
$ch = curl_init('https://api.example.com/v1/orders');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Accept: application/json']]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

Always set a timeout when calling another service. Without one, a slow payment gateway keeps your PHP-FPM worker busy until all workers are stuck and the whole site goes down.

6. Swagger / OpenAPI: documenting the API

OpenAPI is a standard YAML/JSON description of an API: endpoints, parameters, request and response schemas, auth and error codes. Swagger UI renders it as an interactive page where developers can try requests. Other tools generate client SDKs, mock servers and contract tests from the same file.

Two approaches: code-first (annotations in controllers generate the spec; springdoc-openapi, L5-Swagger, Scramble) or design-first (write the spec, review it with consumers, then implement). Payment companies usually go design-first because merchants build against the spec before the API exists.

OpenAPI snippet for a refund endpoint

openapi: 3.0.3
info: { title: Payments API, version: "1.0" }
paths:
  /v1/payments/{paymentId}/refunds:
    post:
      summary: Refund a captured payment (full or partial)
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: paymentId, in: path, required: true, schema: { type: string, example: pay_9X2 } }
        - { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
      requestBody:
        content:
          application/json:
            schema: { type: object, required: [amount], properties: { amount: { type: integer, description: "in paise" } } }
      responses:
        "201": { description: Refund created }
        "409": { description: Payment already fully refunded }
        "422": { description: Amount exceeds refundable balance }
components:
  securitySchemes: { bearerAuth: { type: http, scheme: bearer } }

Spring Boot: springdoc generates /swagger-ui.html from annotations

// implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
@Operation(summary = "Refund a captured payment")
@ApiResponse(responseCode = "201", description = "Refund created")
@ApiResponse(responseCode = "409", description = "Already refunded")
@PostMapping("/v1/payments/{paymentId}/refunds")
RefundDto refund(@PathVariable String paymentId, @Valid @RequestBody RefundRequest body) { ... }

Laravel: L5-Swagger annotations (or Scramble, which infers docs from Form Requests and Resources)

// composer require darkaonline/l5-swagger  ->  php artisan l5-swagger:generate  ->  /api/documentation
/**
 * @OA\Post(path="/api/v1/orders", summary="Place an order", security={{"sanctum":{}}},
 *   @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/PlaceOrder")),
 *   @OA\Response(response=201, description="Created"),
 *   @OA\Response(response=422, description="Validation failed"))
 */
public function store(StoreOrderRequest $request) { ... }

7. Interview questions

PUT vs PATCH?

PUT replaces the whole resource and is idempotent; PATCH changes some fields.

How do you version a REST API?

URL prefix (/v1) is most common and easy to route; headers are cleaner but harder to test. Never break v1; add v2 and deprecate with a timeline.

How do you stop expensive GraphQL queries?

Depth and complexity limits, pagination on lists, persisted queries, timeouts and DataLoader batching.

Offset vs cursor pagination?

OFFSET scans and discards rows, so page 5,000 is slow and rows shift when new data arrives; cursor (WHERE id < last_id) uses the index and is stable.