How to Properly Use POST Links

In web applications, especially in administrative interfaces, it should be a basic rule that actions changing the state of the server should not be performed via the HTTP GET method. As the method name suggests, GET should be used only to retrieve data, not to change it. For actions such as deleting records, it is more appropriate to use the POST method. Although the ideal would be to use the DELETE method, this cannot be invoked without JavaScript, hence POST is historically used.

How to do it in practice? Use this simple trick. At the beginning of your template, create a helper form with the identifier postForm, which you will then use for the delete buttons:

<form method="post" id="postForm"></form>

With this form, you can use a <button> instead of the classic <a> link, which can be visually modified to look like a regular link. For example, the Bootstrap CSS framework offers the classes btn btn-link which allow the button to be visually indistinguishable from other links. Using the form="postForm" attribute, we link it to the pre-prepared form:

<table>
	<tr n:foreach="$posts as $post">
		<td>{$post->title}</td>
		<td>
			<button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">delete</button>
			<!-- instead of <a n:href="delete $post->id">delete</a> -->
		</td>
	</tr>
</table>

When clicking the link, the delete action is now invoked. To ensure that requests are accepted only through the POST method and from the same domain (which is an effective defense against CSRF attacks), use the #[Requires] attribute:

use Nette\Application\Attributes\Requires;

class AdminPresenter extends Nette\Application\UI\Presenter
{
	#[Requires(methods: 'POST', sameOrigin: true)]
	public function actionDelete(int $id): void
	{
		$this->facade->deletePost($id); // hypothetical code for deleting a record
		$this->redirect('default');
	}
}

The attribute has been available since Nette Application 3.2, and you can learn more about its capabilities on the page How to use the #Requires attribute.

If you were using the signal handleDelete() instead of the action actionDelete(), it is not necessary to specify sameOrigin: true, because signals have this protection set implicitly:

#[Requires(methods: 'POST')]
public function handleDelete(int $id): void
{
	$this->facade->deletePost($id);
	$this->redirect('this');
}

This approach not only improves the security of your application but also contributes to adhering to proper web standards and practices. By using POST methods for state-changing actions, you achieve a more robust and secure application.