Insights into Company
← Spring Boot

REST API Development

01 How do you create a REST API?

Solution and explanation

The layers

HTTP request → @RestController (validation, mapping)
             → @Service (business rules, @Transactional)
             → Repository (Spring Data JPA)
             → Database
DTOs carry data in and out; entities stay inside the service/repository layers.

Step 1: entity and repository

@Entity
@Table(name = "jobs")
public class Job {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String location;
    private boolean open = true;
    // getters and setters
}

public interface JobRepository extends JpaRepository<Job, Long> {
    Page<Job> findByLocationIgnoreCaseAndOpenTrue(String location, Pageable pageable);
}

Step 2: DTOs

public record CreateJobRequest(@NotBlank @Size(max = 120) String title, @NotBlank String location) { }
public record JobResponse(Long id, String title, String location, boolean open) {
    static JobResponse from(Job j) { return new JobResponse(j.getId(), j.getTitle(), j.getLocation(), j.isOpen()); }
}

Step 3: service

@Service
public class JobService {
    private final JobRepository jobs;
    public JobService(JobRepository jobs) { this.jobs = jobs; }

    @Transactional
    public JobResponse create(CreateJobRequest req) {
        Job job = new Job();
        job.setTitle(req.title());
        job.setLocation(req.location());
        return JobResponse.from(jobs.save(job));
    }

    @Transactional(readOnly = true)
    public JobResponse get(Long id) {
        return jobs.findById(id).map(JobResponse::from)
                   .orElseThrow(() -> new JobNotFoundException(id));
    }
}

Step 4: controller

@RestController
@RequestMapping("/api/v1/jobs")
public class JobController {
    private final JobService service;
    public JobController(JobService service) { this.service = service; }

    @PostMapping
    public ResponseEntity<JobResponse> create(@Valid @RequestBody CreateJobRequest req) {
        JobResponse created = service.create(req);
        return ResponseEntity.created(URI.create("/api/v1/jobs/" + created.id())).body(created);
    }

    @GetMapping("/{id}")
    public JobResponse get(@PathVariable Long id) { return service.get(id); }
}

Step 5: error handling

@RestControllerAdvice
public class ApiErrors {
    @ExceptionHandler(JobNotFoundException.class)
    ResponseEntity<Map<String, Object>> notFound(JobNotFoundException e) {
        return ResponseEntity.status(404).body(Map.of("code", "JOB_NOT_FOUND", "message", e.getMessage()));
    }
}

Trying it

POST /api/v1/jobs {"title":"Java Developer","location":"Pune"}  → 201 Created, Location: /api/v1/jobs/1
GET  /api/v1/jobs/1                                          → 200 OK
GET  /api/v1/jobs/99                                         → 404 {"code":"JOB_NOT_FOUND",...}

Checklist for a production API

  • Correct HTTP methods and status codes, DTOs instead of entities, validation, central error handling.
  • Pagination for lists, versioning (/v1), authentication (Spring Security + JWT).
  • OpenAPI documentation (springdoc), tests (@WebMvcTest, @SpringBootTest).
Run this example
@RestController
@RequestMapping("/api/v1/jobs")
public class JobController {

    @GetMapping
    public Page<JobDto> index(
        Pageable pageable
    ) {
        return service.index(pageable);
    }
}
Hands-on challenge

Build a paginated jobs endpoint without returning JPA entities directly.

Quick check

Which statement is correct?

02 What are HTTP methods?

Solution and explanation

What they are

HTTP methods (verbs) tell the server what action the client wants on a resource. REST APIs map them to create, read, update and delete operations.

The main methods

  • GET – read a resource or collection. Safe and idempotent. No body.
  • POST – create a resource or trigger an action (send OTP, pay). Not idempotent.
  • PUT – replace a resource completely. Idempotent.
  • PATCH – update part of a resource. Usually not guaranteed idempotent.
  • DELETE – remove a resource. Idempotent.
  • HEAD – like GET but headers only (check existence/size).
  • OPTIONS – what methods are allowed; used by browsers for CORS preflight.

Safe and idempotent

  • Safe: does not change server state (GET, HEAD, OPTIONS).
  • Idempotent: repeating the same request has the same effect as doing it once (GET, PUT, DELETE). Retrying a timed-out PUT is safe; retrying a POST may create duplicates.

Mapping for a candidate profile API

GET    /api/v1/candidates          list candidates
GET    /api/v1/candidates/42       get one
POST   /api/v1/candidates          create   → 201 Created
PUT    /api/v1/candidates/42       replace the whole profile
PATCH  /api/v1/candidates/42       change only phone/headline
DELETE /api/v1/candidates/42       delete   → 204 No Content
@RestController
@RequestMapping("/api/v1/candidates")
class CandidateController {
    @GetMapping List<CandidateDto> all() { ... }
    @GetMapping("/{id}") CandidateDto one(@PathVariable Long id) { ... }
    @PostMapping ResponseEntity<CandidateDto> create(@Valid @RequestBody NewCandidate c) { ... }
    @PutMapping("/{id}") CandidateDto replace(@PathVariable Long id, @Valid @RequestBody NewCandidate c) { ... }
    @PatchMapping("/{id}") CandidateDto patch(@PathVariable Long id, @RequestBody Map<String, Object> changes) { ... }
    @DeleteMapping("/{id}") ResponseEntity<Void> delete(@PathVariable Long id) { ... }
}

Making POST safe to retry (payments)

Clients send an Idempotency-Key header; the server stores the first result for that key and returns it for repeats, so a retried "pay" request never charges twice.

Note: actions that are not CRUD (approve, cancel, refund) are usually modelled as POST /orders/{id}/cancel.

Run this example
GET    /api/jobs
POST   /api/jobs
PUT    /api/jobs/42
PATCH  /api/jobs/42
DELETE /api/jobs/42
Hands-on challenge

Design CRUD routes for job applications and identify which operations should be idempotent.

Quick check

Which statement is correct?

03 PUT versus PATCH

Solution and explanation

The difference

  • PUT replaces the entire resource with the representation you send. Fields you omit are cleared (set to null/default). Idempotent.
  • PATCH applies a partial change: only the fields you send are modified. Not guaranteed idempotent (e.g. "increment views").

Example: a candidate profile

Current state:

{"id": 42, "name": "Asha", "phone": "98100...", "city": "Pune", "headline": "Java Developer"}

PUT with only name and city:

PUT /api/v1/candidates/42
{"name": "Asha Rao", "city": "Bengaluru"}

Result: phone and headline become null, because PUT replaces everything.

PATCH with only city:

PATCH /api/v1/candidates/42
{"city": "Bengaluru"}

Result: only city changes; everything else stays.

Implementing PATCH in Spring

public record CandidatePatch(Optional<String> phone, Optional<String> city, Optional<String> headline) { }

@PatchMapping("/{id}")
@Transactional
public CandidateDto patch(@PathVariable Long id, @RequestBody CandidatePatch patch) {
    Candidate c = candidates.findById(id).orElseThrow();
    patch.phone().ifPresent(c::setPhone);
    patch.city().ifPresent(c::setCity);
    patch.headline().ifPresent(c::setHeadline);
    return CandidateDto.from(c);              // dirty checking saves the changes on commit
}

Implementing PUT

@PutMapping("/{id}")
@Transactional
public CandidateDto replace(@PathVariable Long id, @Valid @RequestBody CandidateRequest body) {
    Candidate c = candidates.findById(id).orElseThrow();
    c.setName(body.name()); c.setPhone(body.phone()); c.setCity(body.city()); c.setHeadline(body.headline());
    return CandidateDto.from(c);
}

Which to use

  • Forms that always submit the full object → PUT.
  • Mobile apps editing one field, toggles, status changes → PATCH.

Note: JSON Merge Patch (application/merge-patch+json) and JSON Patch (application/json-patch+json, a list of operations) are standard PATCH formats for more complex needs.

Run this example
@PatchMapping("/{id}")
public JobDto updateStatus(
    @PathVariable Long id,
    @RequestBody UpdateStatusRequest request
) {
    return service.updateStatus(
        id,
        request
    );
}
Hands-on challenge

Implement a PATCH endpoint that changes only the application status.

Quick check

Which statement is correct?

04 What is ResponseEntity?

Solution and explanation

What it is

ResponseEntity<T> represents the whole HTTP response: status code, headers and body. Returning it from a controller gives you full control instead of the default 200 OK.

@PostMapping("/api/v1/applications")
public ResponseEntity<ApplicationDto> apply(@Valid @RequestBody ApplyRequest req) {
    ApplicationDto created = service.apply(req);
    return ResponseEntity
            .created(URI.create("/api/v1/applications/" + created.id()))   // 201 + Location header
            .header("X-Request-Id", MDC.get("requestId"))
            .body(created);
}

@GetMapping("/api/v1/applications/{id}")
public ResponseEntity<ApplicationDto> get(@PathVariable Long id) {
    return service.find(id)
            .map(ResponseEntity::ok)                    // 200 with body
            .orElse(ResponseEntity.notFound().build()); // 404 without body
}

@DeleteMapping("/api/v1/applications/{id}")
public ResponseEntity<Void> withdraw(@PathVariable Long id) {
    service.withdraw(id);
    return ResponseEntity.noContent().build();          // 204
}

Useful builders

  • ok(body), created(uri), accepted(), noContent(), badRequest(), notFound(), status(HttpStatus.CONFLICT).
  • header(name, value), contentType(MediaType.APPLICATION_PDF), eTag(...), cacheControl(...).

File download example

@GetMapping("/api/v1/resumes/{id}/download")
public ResponseEntity<Resource> download(@PathVariable Long id) {
    Resource file = storage.load(id);
    return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_PDF)
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"resume.pdf\"")
            .body(file);
}

When you do not need it

Returning a plain object gives 200 automatically; @ResponseStatus(HttpStatus.CREATED) on the method changes the default status. Use ResponseEntity when status or headers depend on the outcome.

Run this example
URI location =
    URI.create(
        "/api/v1/jobs/" + job.id()
    );

return ResponseEntity
    .created(location)
    .body(job);
Hands-on challenge

Return 201 Created with a Location header after creating a job.

Quick check

Which statement is correct?

05 Which HTTP status codes should APIs use?

Solution and explanation

Families

  • 2xx success, 3xx redirection, 4xx client error (fix the request), 5xx server error (retry may help).

The codes you will use most

  • 200 OK – successful GET/PUT/PATCH with a body.
  • 201 Created – a POST created a resource; include a Location header.
  • 202 Accepted – work queued for later (bulk import, report generation).
  • 204 No Content – success without a body (DELETE).
  • 400 Bad Request – malformed JSON, invalid parameters, validation failure (some APIs use 422 for validation).
  • 401 Unauthorized – not authenticated (missing or expired token).
  • 403 Forbidden – authenticated but not allowed.
  • 404 Not Found – resource does not exist (or is hidden from this user).
  • 405 Method Not Allowed – path exists, method does not.
  • 409 Conflict – state conflict: duplicate email, applying twice, order already shipped.
  • 415 Unsupported Media Type – wrong Content-Type.
  • 422 Unprocessable Entity – well-formed but semantically invalid.
  • 429 Too Many Requests – rate limit hit; include Retry-After.
  • 500 Internal Server Error – unexpected bug; never expose stack traces.
  • 502/503/504 – upstream service or gateway problems; clients may retry with backoff.

Mapping exceptions to codes in Spring

@RestControllerAdvice
public class ApiExceptionHandler {
    @ExceptionHandler(ResourceNotFoundException.class)
    ResponseEntity<ApiError> notFound(ResourceNotFoundException e) { return error(404, "NOT_FOUND", e.getMessage()); }

    @ExceptionHandler(DuplicateApplicationException.class)
    ResponseEntity<ApiError> conflict(DuplicateApplicationException e) { return error(409, "ALREADY_APPLIED", e.getMessage()); }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    ResponseEntity<ApiError> invalid(MethodArgumentNotValidException e) { return error(400, "VALIDATION_FAILED", "Check the highlighted fields"); }

    private ResponseEntity<ApiError> error(int status, String code, String message) {
        return ResponseEntity.status(status).body(new ApiError(code, message, Instant.now()));
    }
}

Note: the status code and body must agree – never return 200 with {"error": ...}; clients, proxies and monitoring rely on the status.

Run this example
return ResponseEntity
    .noContent()
    .build();
Hands-on challenge

Map five job-portal scenarios to appropriate HTTP status codes.

Quick check

Which statement is correct?

06 How do you validate API requests?

Solution and explanation

Bean Validation in Spring Boot

Add spring-boot-starter-validation, annotate DTO fields with constraints, and put @Valid on the @RequestBody (or @Validated on the controller for path/query parameters).

public record RegisterCandidateRequest(
        @NotBlank @Size(max = 80) String name,
        @NotBlank @Email String email,
        @Pattern(regexp = "^[6-9]\\d{9}$", message = "must be a valid Indian mobile number") String phone,
        @Min(0) @Max(50) Integer experienceYears,
        @Past LocalDate dateOfBirth,
        @NotEmpty List<@NotBlank String> skills) { }

@PostMapping("/api/v1/candidates")
public ResponseEntity<CandidateDto> register(@Valid @RequestBody RegisterCandidateRequest req) { ... }

Common constraints

@NotNull, @NotBlank, @NotEmpty, @Size, @Min/@Max, @Positive, @Email, @Pattern, @Past/@Future, @Digits, @DecimalMin. Use @Valid on nested objects and list elements.

Returning field errors clearly

@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<Map<String, Object>> handle(MethodArgumentNotValidException ex) {
    Map<String, String> fields = new LinkedHashMap<>();
    ex.getBindingResult().getFieldErrors().forEach(e -> fields.put(e.getField(), e.getDefaultMessage()));
    return ResponseEntity.badRequest().body(Map.of("code", "VALIDATION_FAILED", "errors", fields));
}
400 Bad Request
{"code":"VALIDATION_FAILED","errors":{"email":"must be a well-formed email address","phone":"must be a valid Indian mobile number"}}

Validating path and query parameters

@Validated
@RestController
class JobController {
    @GetMapping("/api/v1/jobs")
    List<JobDto> search(@RequestParam @Min(1) @Max(100) int size) { ... }   // ConstraintViolationException
}

Custom validation

@Target(FIELD) @Retention(RUNTIME) @Constraint(validatedBy = PanValidator.class)
public @interface ValidPan { String message() default "invalid PAN"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }

public class PanValidator implements ConstraintValidator<ValidPan, String> {
    public boolean isValid(String v, ConstraintValidatorContext c) { return v == null || v.matches("[A-Z]{5}[0-9]{4}[A-Z]"); }
}

Note: validation of format belongs in DTOs; business rules (email already registered, job closed) belong in the service and usually return 409 or 422.

Run this example
public record CreateUserRequest(

    @NotBlank
    String name,

    @Email
    String email

) {
}

@PostMapping
public UserDto create(
    @Valid
    @RequestBody
    CreateUserRequest request
) {
    return service.create(request);
}
Hands-on challenge

Test blank names and malformed email addresses and inspect the validation response.

Quick check

Which statement is correct?

07 What is global exception handling?

Solution and explanation

What it is

Instead of try/catch in every controller, a single @RestControllerAdvice class handles exceptions thrown from any controller and turns them into consistent HTTP responses.

@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiError> notFound(ResourceNotFoundException ex) {
        return build(HttpStatus.NOT_FOUND, "NOT_FOUND", ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiError> validation(MethodArgumentNotValidException ex) {
        String msg = ex.getBindingResult().getFieldErrors().stream()
                .map(e -> e.getField() + " " + e.getDefaultMessage()).collect(Collectors.joining("; "));
        return build(HttpStatus.BAD_REQUEST, "VALIDATION_FAILED", msg);
    }

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ApiError> forbidden(AccessDeniedException ex) {
        return build(HttpStatus.FORBIDDEN, "FORBIDDEN", "You cannot access this resource");
    }

    @ExceptionHandler(Exception.class)                          // safety net
    public ResponseEntity<ApiError> unexpected(Exception ex) {
        log.error("Unhandled error", ex);                       // full stack trace only in logs
        return build(HttpStatus.INTERNAL_SERVER_ERROR, "SERVER_ERROR", "Something went wrong");
    }

    private ResponseEntity<ApiError> build(HttpStatus status, String code, String message) {
        return ResponseEntity.status(status).body(new ApiError(code, message, Instant.now()));
    }
}

public record ApiError(String code, String message, Instant timestamp) { }

How Spring chooses a handler

The most specific matching @ExceptionHandler wins (ResourceNotFoundException before Exception). Handlers can also live in a single controller for local rules.

ProblemDetail (Spring 6 / Boot 3)

A standard error format (RFC 7807):

@ExceptionHandler(ResourceNotFoundException.class)
ProblemDetail notFound(ResourceNotFoundException ex) {
    ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    pd.setTitle("Job not found");
    return pd;
}

Benefits

Consistent error shape for clients, no duplicated try/catch, no stack traces leaking to users, one place for logging and monitoring.

Run this example
@RestControllerAdvice
public class ApiErrors {

    @ExceptionHandler(
        JobNotFoundException.class
    )
    ResponseEntity<ApiError> missing(
        JobNotFoundException ex
    ) {
        return ResponseEntity
            .status(404)
            .body(
                ApiError.of(
                    ex.getMessage()
                )
            );
    }
}
Hands-on challenge

Handle validation, not-found and conflict errors through one centralized advice class.

Quick check

Which statement is correct?

08 How do you create a custom exception?

Solution and explanation

Why custom exceptions

They give business failures a name (JobClosedException, InsufficientBalanceException) so the service layer can signal them clearly and the global handler can map each to the right status code and error code.

Step 1: define the exception

public class JobClosedException extends RuntimeException {
    private final Long jobId;

    public JobClosedException(Long jobId) {
        super("Job " + jobId + " is no longer accepting applications");
        this.jobId = jobId;
    }

    public Long getJobId() { return jobId; }
}

Extend RuntimeException (unchecked) for business errors: no throws clauses everywhere, and @Transactional rolls back on unchecked exceptions by default.

Step 2: throw it from the service

@Transactional
public ApplicationDto apply(Long jobId, Long candidateId) {
    Job job = jobs.findById(jobId).orElseThrow(() -> new ResourceNotFoundException("Job", jobId));
    if (!job.isOpen()) throw new JobClosedException(jobId);
    if (applications.existsByJobIdAndCandidateId(jobId, candidateId)) throw new DuplicateApplicationException(jobId);
    return ApplicationDto.from(applications.save(new Application(job, candidateId)));
}

Step 3: map it to HTTP

@ExceptionHandler(JobClosedException.class)
ResponseEntity<ApiError> closed(JobClosedException ex) {
    return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(new ApiError("JOB_CLOSED", ex.getMessage(), Instant.now()));
}

Or annotate the exception itself: @ResponseStatus(HttpStatus.CONFLICT).

A small hierarchy keeps handlers simple

public abstract class BusinessException extends RuntimeException {
    private final String code; private final HttpStatus status;
    protected BusinessException(String code, HttpStatus status, String message) { super(message); this.code = code; this.status = status; }
    public String code() { return code; } public HttpStatus status() { return status; }
}
public class JobClosedException extends BusinessException {
    public JobClosedException(Long id) { super("JOB_CLOSED", HttpStatus.CONFLICT, "Job " + id + " is closed"); }
}

@ExceptionHandler(BusinessException.class)
ResponseEntity<ApiError> business(BusinessException ex) {
    return ResponseEntity.status(ex.status()).body(new ApiError(ex.code(), ex.getMessage(), Instant.now()));
}

Note: keep the original cause when wrapping technical errors: new PaymentFailedException("Gateway timeout", cause).

Run this example
public class JobNotFoundException
        extends RuntimeException {

    public JobNotFoundException(Long id) {
        super(
            "Job " + id +
            " was not found"
        );
    }
}
Hands-on challenge

Throw JobNotFoundException when a requested job does not exist.

Quick check

Which statement is correct?

09 How should APIs return errors?

Solution and explanation

Principles

  1. Use the correct HTTP status code.
  2. Return a consistent JSON structure for every error.
  3. Include a stable, machine-readable code that clients can switch on.
  4. Give a human-readable message, plus field-level details for validation.
  5. Add a trace/request id so support can find the logs.
  6. Never expose stack traces, SQL or internal class names.

A consistent error body

{
  "code": "VALIDATION_FAILED",
  "message": "Some fields are invalid",
  "status": 400,
  "path": "/api/v1/candidates",
  "timestamp": "2026-10-12T10:20:30Z",
  "traceId": "8f2c1e7a9b",
  "errors": [
    {"field": "email", "message": "must be a well-formed email address"},
    {"field": "experienceYears", "message": "must be less than or equal to 50"}
  ]
}

Error codes clients can rely on

  • VALIDATION_FAILED (400), UNAUTHENTICATED (401), FORBIDDEN (403), NOT_FOUND (404), ALREADY_APPLIED (409), RATE_LIMITED (429), SERVER_ERROR (500).

A React or mobile app shows a friendly message per code and highlights fields from errors[].

Implementation in Spring

public record ApiError(String code, String message, int status, String path, Instant timestamp,
                       String traceId, List<FieldError> errors) {
    public record FieldError(String field, String message) { }
}

Build it in @RestControllerAdvice for every exception type (see global exception handling), taking the path from HttpServletRequest and the trace id from MDC.

Payment API example

{"code": "PAYMENT_DECLINED", "message": "The bank declined the card", "status": 402,
 "decline_reason": "insufficient_funds", "retryable": false}

Telling the client whether a retry makes sense prevents duplicate charges.

Note: Spring Boot 3's ProblemDetail follows RFC 7807 and is a good standard shape; extend it with your own properties.

Run this example
public record ApiError(
    Instant timestamp,
    int status,
    String code,
    String message,
    String path
) {
}
Hands-on challenge

Create one structured response for a missing job and another for invalid request fields.

Quick check

Which statement is correct?

10 What is API versioning?

Solution and explanation

Why it is needed

Mobile apps and partner integrations cannot all update at the same moment. When you make a breaking change (renaming a field, changing a type, removing an endpoint), existing clients would fail. Versioning lets old and new contracts run side by side.

Breaking vs non-breaking changes

  • Non-breaking: adding a new optional field or endpoint – no new version needed.
  • Breaking: removing or renaming fields, changing types or meanings, new required inputs – needs a new version.

Strategies

  1. URI path (most common, easy to test and route):
@RestController @RequestMapping("/api/v1/jobs") class JobControllerV1 { ... }
@RestController @RequestMapping("/api/v2/jobs") class JobControllerV2 { ... }
  1. Request parameter: /api/jobs?version=2.
  2. Custom header: X-API-Version: 2
@GetMapping(value = "/api/jobs", headers = "X-API-Version=2")
public List<JobV2> listV2() { ... }
  1. Media type (content negotiation): Accept: application/vnd.ascendia.v2+json
@GetMapping(value = "/api/jobs", produces = "application/vnd.ascendia.v2+json")
public List<JobV2> listV2() { ... }

Example of a breaking change

v1 returns {"salary": "8-12 LPA"}; v2 returns {"salary": {"min": 800000, "max": 1200000, "currency": "INR"}}. Both versions read from the same service; only the DTO mapping differs.

Lifecycle

  • Announce deprecation (Deprecation and Sunset headers, docs, emails).
  • Monitor traffic per version; retire v1 after clients migrate.
  • Keep the number of live versions small (usually 2).

Note: share business logic between versions – only controllers and DTOs should be version-specific.

Run this example
@RequestMapping("/api/v1/jobs")
public class JobV1Controller {

}
Hands-on challenge

Introduce a v2 DTO without silently breaking existing v1 consumers.

Quick check

Which statement is correct?

Knowledge Check

Spring Quiz: Lessons 1 – 10

Test what you have learned before continuing to the next learning block.

1. How do you create a REST API?

Which statement is correct?

2. What are HTTP methods?

Which statement is correct?

3. PUT versus PATCH

Which statement is correct?

4. What is ResponseEntity?

Which statement is correct?

5. Which HTTP status codes should APIs use?

Which statement is correct?

6. How do you validate API requests?

Which statement is correct?

7. What is global exception handling?

Which statement is correct?

8. How do you create a custom exception?

Which statement is correct?

9. How should APIs return errors?

Which statement is correct?

10. What is API versioning?

Which statement is correct?