DEV Community

mdshlnwz
mdshlnwz

Posted on

Proposal: Strict REST Mappings — Making REST APIs Foolproof with Annotations and Interfaces

Hey Devs! 👋
Have you ever seen a DELETE endpoint sneakily creating a resource or a GET request updating data? 🙈 These REST API missteps are all too common, and they lead to confusion, bugs, and APIs that are hard to maintain. Let’s dive into this problem and propose a solution to enforce REST-compliant logic in our favorite web frameworks. I’m excited to share my idea for Strict REST Mappings and get your feedback to make it even better! 🚀
🚨 The Problem: REST APIs Gone WildModern web frameworks like Spring Boot, Express.js, Django REST, and FastAPI give us incredible flexibility to map any logic to any HTTP method. But with great power comes great responsibility—and sometimes, great chaos.

😅Here’s a real-world example in Spring Boot:@DeleteMapping("/users/{id}")
public void doSomethingWeird(@PathVariable Long id) {
userService.createUser(new User()); // ❌ Creating a user in a DELETE endpoint?!
userService.delete(id);
}

This code violates REST principles, where DELETE should only remove a resource idempotently. Similar misuses happen with GET doing updates, POST deleting resources, or PUT creating new ones. This leads to:
😕 Confusion for Beginners: New devs struggle to understand what each HTTP method should do, leading to bad habits.
❌ REST Violations: APIs deviate from REST standards, making them unpredictable and hard to integrate.
🔞 Poor Maintainability: Teams waste time debugging and documenting APIs that don’t follow expected behavior.Frameworks don’t enforce REST semantics, leaving it to developers to “do the right thing.” But what if we could make frameworks smarter, catching these mistakes early and guiding devs to build better REST APIs? 🤔

✅ The Solution: Strict REST Mappings with Annotations and InterfacesI propose Strict REST Mappings, a system to enforce REST-compliant logic for each HTTP method using annotations and interfaces. The idea is simple: provide strict annotations like @RestrictGet, @RestrictPost, @RestrictPut, and @RestrictDelete that ensure methods follow REST principles, while keeping standard annotations (e.g., @GetMapping, @DeleteMapping) for flexibility within REST boundaries.

💡 Core Concept
Annotations: Strict annotations enforce specific REST behaviors:
@RestrictGet: Only read operations (e.g., fetching data).
@RestrictPost: Only create new resources.
@RestrictPut: Only update resources idempotently.
@RestrictDelete: Only delete or mark resources as deleted.
Interfaces: Contracts like Readable, Creatable, Updatable, and Deletable define the allowed logic for each HTTP method.
Enforcement: Use compile-time checks (e.g., annotation processors), runtime checks (e.g., AOP), or IDE plugins to flag violations early.

💠 Design in ActionLet’s focus on the DELETE case with @RestrictDelete and the Deletable interface, which ensures REST-compliant deletion logic.

  1. Define the Deletable Interface
    public interface Deletable {
    void delete(Long id); // Deletes a resource by ID
    }
    This interface specifies what resource to delete, ensuring DELETE endpoints are idempotent and REST-compliant.

  2. Implement the InterfaceA service implements Deletable to define deletion logic:

@Service
public class UserService implements Deletable {
private final UserRepository userRepository;

public UserService (UserRepository userRepository) {
    this.userRepository = userRepository;
}

@Override
public void delete(Long id) {
    User user = userRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User not found: " + id));
    userRepository.delete(user); // REST-compliant deletion
}
Enter fullscreen mode Exit fullscreen mode

}

  1. Use @RestrictDelete in a Controller

@RestController
@RequestMapping("/users")
public class UserController implements Deletable {
private final UserService userService;
private final AuditService auditService;

public UserController (UserService userService, AuditService auditService) {
    this.userService = userService;
    this.auditService = auditService;
}

// Strict REST-compliant DELETE
@RestrictDelete
@DeleteMapping("/{id}")
@Override
public void delete(@PathVariable Long id) {
    userService.delete(id); // Only deletion allowed
}

// Flexible REST endpoint
@DeleteMapping("/{id}/with-audit")
public void deleteWithAudit(@PathVariable Long id) {
    userService.delete(id);
    auditService.logAction("Deleted user " + id); // REST-compliant side effect
}
Enter fullscreen mode Exit fullscreen mode

}

  1. Enforce with an Annotation ProcessorA Java annotation processor ensures @RestrictDelete methods only call Deletable.delete. Here’s a simplified version:

@SupportedAnnotationTypes("RestrictDelete")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class RestrictDeleteProcessor extends AbstractProcessor {
private Types typeUtils;
private Elements elementUtils;
private Messager messager;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    this.typeUtils = processingEnv.getTypeUtils();
    this.elementUtils = processingEnv.getElementUtils();
    this.messager = processingEnv.getMessager();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for (Element element : roundEnv.getElementsAnnotatedWith(RestrictDelete.class)) {
        if (element.getKind() != ElementKind.METHOD) {
            error(element, "@RestrictDelete can only be applied to methods");
            continue;
        }

        ExecutableElement method = (ExecutableElement) element;
        TypeElement enclosingClass = (TypeElement) method.getEnclosingElement();

        // Check if class implements Deletable
        if (!implementsDeletable(enclosingClass)) {
            error(method, "Class with @RestrictDelete must implement Deletable");
            return true;
        }

        // Simplified: Check method calls Deletable.delete
        if (!method.getSimpleName().toString().equals("delete")) {
            error(method, "@RestrictDelete method must only call Deletable.delete");
            return true;
        }
    }
    return true;
}

private boolean implementsDeletable(TypeElement classElement) {
    TypeMirror deletableType = elementUtils.getTypeElement("Deletable").asType();
    return typeUtils.isAssignable(classElement.asType(), typeUtils.erasure(deletableType));
}

private void error(Element element, String message) {
    messager.printMessage(Diagnostic.Kind.ERROR, message, element);
}
Enter fullscreen mode Exit fullscreen mode

}

What It Does: The processor fails compilation if a @RestrictDelete method doesn’t implement Deletable.delete or includes non-delete logic (e.g., userService.create()).

REST Benefit: Ensures DELETE endpoints strictly remove resources, preventing REST violations.

  1. Handling REST-Compliant Edge CasesSometimes, REST APIs need side effects like logging or notifications. Use @DeleteMapping for these cases:// This would fail compilation

@RestrictDelete
@DeleteMapping("/invalid/{id}")
public void invalidDelete(@PathVariable Long id) {
userService.create(new User()); // ❌ Non-delete logic
userService.delete(id);
}

// Flexible REST endpoint
@DeleteMapping("/{id}/with-audit")
public void deleteWithAudit(@PathVariable Long id) {
userService.delete(id);
auditService.logAction("Deleted user " + id); // ✅ REST-compliant side effect
}
@RestrictDelete: Strict deletion only, enforcing REST’s idempotent removal.
@DeleteMapping: Allows REST-compliant side effects, like logging, which are common in real-world REST APIs.

🚀 Why This Matters for REST APIs

This proposal is all about making REST APIs better—not supporting non-REST paradigms like GraphQL or gRPC.
Here’s how Strict REST Mappings improve REST API architecture:

👶 Guides Beginners: Annotations like @RestrictDelete and interfaces like Deletable teach new devs the right way to use HTTP methods.

🏢 Ensures Team Consistency: Enforces uniform REST behavior across endpoints, making APIs predictable and easier to maintain.

🚀 Prevents REST Violations: Catches misuse (e.g., creating in a DELETE endpoint) at compile-time, reducing bugs.

📈 Enhances Tooling: Enables linters and IDE plugins to provide real-time feedback, improving code quality and OpenAPI documentation accuracy.

🌐 Where Can This Work?This concept can be applied to popular REST frameworks:
Spring Boot (Java): Use annotations and processors, as shown above.
FastAPI (Python): Implement with decorators and Protocols.
NestJS (TypeScript): Use decorators and interfaces.
Express.js (JavaScript): Use middleware for runtime checks.
Django REST (Python): Use custom view decorators.

Here’s a quick FastAPI example for @RestrictDelete:from fastapi import FastAPI
from typing import Protocol

class Deletable(Protocol):
def delete(self, id: int) -> None:
pass

class UserService:
def delete(self, id: int) -> None:
print(f"REST-compliant deletion of user {id}")

app = FastAPI()
user_service = UserService()

from functools import wraps
def restrict_delete(func):
@wraps(func)
async def wrapper(*args, **kwargs):
if not isinstance(kwargs.get("self"), Deletable):
raise Exception("@restrict_delete requires Deletable implementation")
return await func(*args, **kwargs)
return wrapper

class UserController:
@restrict_delete
@app.delete("/users/{id}", status_code=204)
async def delete(self, id: int) -> None:
user_service.delete(id)

📣 Call to Action: Let’s Build This Together!
I believe Strict REST Mappings can transform how we build REST APIs, making them more reliable, beginner-friendly, and true to REST principles.
But I need your input to make it happen!
🙌Would you use @RestrictDelete, @RestrictGet, etc., in your projects?
How would you implement this in your favorite framework (e.g., FastAPI, Express)?
What challenges do you see, and how can we address them?
Want to collaborate?
Share your thoughts in the comments, and let’s start a discussion! 💬Authored by: Shahnawaz
GitHub: github.com/mdshlnwz

Top comments (0)