Como criar um endpoint REST API customizado no Magento 2
O Magento 2 constrói endpoints REST a partir de service interfaces. Você não escreve controllers, não faz parse de JSON nem serializa respostas por conta própria. Você declara uma rota no webapi.xml, aponta para um método de uma interface e o framework de Web API cuida do mapeamento de entrada, da serialização da saída, da autenticação e da autorização. Quando funciona, economiza muito código. Quando não funciona, os erros são vagos, então ajuda saber o que o framework espera.
Este guia monta uma pequena API para uma entidade notice customizada no Magento Open Source ou Adobe Commerce 2.4.8. Ele assume que a entidade já tem um repository, como construído em o artigo sobre service contracts.
As peças que você precisa
- Uma service interface em
Api/com docblocks completos. - Data interfaces em
Api/Data/para qualquer coisa mais complexa que escalares. - Uma preference em
etc/di.xmlde cada interface para sua implementação. - Recursos de ACL em
etc/acl.xmlpara rotas protegidas de admin. - Rotas em
etc/webapi.xml.
Uma service interface para lógica customizada
Expor o repository cobre o CRUD básico. Operações de negócio normalmente merecem a sua própria interface. Aqui vai um management service com dois métodos, um para admins e outro público:
<?php
declare(strict_types=1);
namespace Acme\Notice\Api;
interface NoticeManagementInterface
{
/**
* Activate a notice and deactivate all others.
*
* @param int $noticeId
* @return \Acme\Notice\Api\Data\NoticeInterface
* @throws \Magento\Framework\Exception\NoSuchEntityException
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function activate(int $noticeId);
/**
* Return the active notice messages for the current store.
*
* @return string[]
*/
public function getActiveMessages(): array;
}
O framework de Web API não usa os tipos nativos do PHP para decidir como converter dados. Ele lê as anotações @param e @return por reflection. Regras que poupam tempo de debug:
- Use nomes de classe totalmente qualificados nos docblocks, com a barra invertida no início.
- Use
string[],int[]ou\Vendor\Module\Api\Data\SomeInterface[]para arrays. Um retornoarraysem tipo não vai serializar do jeito que você espera. - Não retorne arrays associativos. Defina uma data interface com getters.
- Todo nome de parâmetro na interface vira uma chave no corpo da request ou um placeholder na URL.
A implementação é uma classe normal. Ela pode injetar o repository, o StoreManagerInterface e tudo mais que precisar. Registre-a no di.xml:
<preference for="Acme\Notice\Api\NoticeManagementInterface" type="Acme\Notice\Model\NoticeManagement"/>
Recursos de ACL
O acesso de admin e de integrações é controlado por recursos de ACL, a mesma árvore usada nos roles de admin. Declare-os em etc/acl.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
<acl>
<resources>
<resource id="Magento_Backend::admin">
<resource id="Acme_Notice::notice" title="Store Notices" sortOrder="100">
<resource id="Acme_Notice::notice_read" title="View Notices" sortOrder="10"/>
<resource id="Acme_Notice::notice_write" title="Manage Notices" sortOrder="20"/>
</resource>
</resource>
</resources>
</acl>
</config>
Separar leitura e escrita vale o pequeno esforço extra. Integrações para ferramentas de relatório devem receber só acesso de leitura, e você não consegue separar isso depois sem mudar todas as integrações que já existem.
Rotas no webapi.xml
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route url="/V1/acme/notices/:noticeId" method="GET">
<service class="Acme\Notice\Api\NoticeRepositoryInterface" method="getById"/>
<resources>
<resource ref="Acme_Notice::notice_read"/>
</resources>
</route>
<route url="/V1/acme/notices/search" method="GET">
<service class="Acme\Notice\Api\NoticeRepositoryInterface" method="getList"/>
<resources>
<resource ref="Acme_Notice::notice_read"/>
</resources>
</route>
<route url="/V1/acme/notices" method="POST">
<service class="Acme\Notice\Api\NoticeRepositoryInterface" method="save"/>
<resources>
<resource ref="Acme_Notice::notice_write"/>
</resources>
</route>
<route url="/V1/acme/notices/:noticeId/activate" method="POST">
<service class="Acme\Notice\Api\NoticeManagementInterface" method="activate"/>
<resources>
<resource ref="Acme_Notice::notice_write"/>
</resources>
</route>
<route url="/V1/acme/notices/active" method="GET">
<service class="Acme\Notice\Api\NoticeManagementInterface" method="getActiveMessages"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
</routes>
Alguns detalhes sobre o roteamento:
:noticeIdna URL mapeia para o parâmetro$noticeIddo método. Os nomes precisam bater.- Um segmento estático como
searche um placeholder como:noticeIdna mesma posição podem entrar em conflito para o mesmo método HTTP. Chame as duas rotas depois de adicioná-las para confirmar que cada uma chega no método que você espera. - O recurso
anonymoustorna a rota pública. Qualquer pessoa pode chamá-la, então trate como uma página pública e nunca exponha dados privados ou operações caras ali.
Rotas por cliente com self
Para rotas que agem sobre o cliente logado, use o recurso self e force o ID do cliente a partir do token:
<route url="/V1/acme/customers/me/notices" method="GET">
<service class="Acme\Notice\Api\CustomerNoticeInterface" method="getForCustomer"/>
<resources>
<resource ref="self"/>
</resources>
<data>
<parameter name="customerId" force="true">%customer_id%</parameter>
</data>
</route>
Com force="true", o valor de customerId sempre vem do cliente autenticado, mesmo que o client envie outro ID na request. Sem isso, um cliente poderia ler os dados de outro cliente mudando um parâmetro. Esse é um dos erros de segurança mais comuns em APIs customizadas. Como o token em si é obtido está em o artigo sobre autenticação da Web API.
Chamando o endpoint
Depois do bin/magento setup:upgrade e de uma limpeza de cache (ou setup:di:compile em modo production), as rotas estão no ar:
TOKEN="your-admin-or-integration-token"
curl -s -H "Authorization: Bearer $TOKEN" \
https://example.test/rest/V1/acme/notices/5
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"notice": {"message": "Free shipping this week", "is_active": true}}' \
https://example.test/rest/V1/acme/notices
curl -s "https://example.test/rest/V1/acme/notices/search?searchCriteria[pageSize]=20&searchCriteria[filterGroups][0][filters][0][field]=is_active&searchCriteria[filterGroups][0][filters][0][value]=1"
curl -s https://example.test/rest/V1/acme/notices/active
As chaves do corpo da request são os nomes dos parâmetros do método, e as chaves de objetos aninhados são convertidas de snake_case para chamadas de setter. is_active chama setIsActive(). Se uma chave não tem setter correspondente, ela é ignorada sem erro, o que facilita deixar passar erros de digitação.
O prefixo da URL controla o scope da store. /rest/V1/... roda na store view padrão, /rest/de/V1/... roda na store view com code de e /rest/all/V1/... é usado para operações de admin que não devem ficar presas a uma store. Salvar produtos por uma URL específica de store é um jeito conhecido de criar valores de atributo no nível da store sem querer.
A documentação gerada em /swagger lista as suas rotas com seus schemas. Se uma rota não aparece ali, o Magento não a carregou, geralmente por um erro de cache ou de XML.
Erros e status codes
Exceções lançadas pelo service são convertidas em respostas HTTP. Uma NoSuchEntityException vira 404 e uma LocalizedException vira 400 com a sua mensagem. Qualquer outra exceção vira 500, e em modo production a mensagem é trocada por uma genérica, enquanto os detalhes vão para o log de exceções. Então lance subclasses de LocalizedException com mensagens claras para os erros que o client deve ver, e deixe os erros inesperados genéricos.
Versionando a API
O V1 na URL é uma convenção, não algo que o framework impõe. Ele importa quando sistemas externos passam a depender do seu endpoint. Se você precisa mudar o formato da request ou da response de um jeito que quebra clients existentes, adicione uma rota nova em /V2/ apontando para um novo método de interface e mantenha a rota antiga funcionando até todos os clients migrarem. Adicionar campos opcionais a uma data interface costuma ser seguro. Renomear ou remover não é, porque os clients que enviam a chave antiga vão tê-la ignorada em silêncio.
Notas de performance e segurança
- Respostas REST não são cacheadas pelo full page cache. Um GET anônimo pesado roda PHP a cada chamada. Se os dados podem ser cacheados, faça o cache você mesmo com o cache frontend e uma tag customizada.
- Defina um tamanho máximo de página na sua implementação de
getList. Não confie que o client vai enviar um valor sensato. - Mantenha endpoints anônimos baratos e aplique rate limit no servidor web ou na CDN.
- Para dados que precisam aparecer em entidades do core nas respostas da API, prefira extension attributes a novos endpoints. Veja o artigo sobre extension attributes.
- Se o consumidor é uma loja e não um sistema de backend, GraphQL pode se encaixar melhor.
Para testes automatizados, o Magento traz um framework de testes funcionais de API em dev/tests/api-functional. Ele monta uma instalação real e chama as suas rotas por HTTP, o que pega erros de docblock e de serialização que os unit tests deixam passar.
FAQ
Por que meu endpoint REST retorna 404 mesmo com o webapi.xml parecendo certo?
Confira se o módulo está habilitado, se o cache de config está limpo e se o XML é válido. Depois procure a rota em /swagger. Se não estiver lá, o Magento não carregou o seu webapi.xml.
Por que alguns campos do corpo da minha request são ignorados?
A Web API mapeia chaves do JSON para setters da data interface. Uma chave sem setter correspondente, ou com erro de digitação em snake_case, é descartada em silêncio.
Como deixo um endpoint disponível só para clientes logados?
Use o recurso self e force o ID do cliente a partir do token com um parâmetro data definido como %customer_id%. Nunca pegue o ID do cliente da request.
Posso retornar um array associativo de um método REST?
Não de forma confiável. Defina uma data interface com getters e retorne ela, ou retorne uma lista tipada como string[].
Atendo como freelancer em Magento e Adobe Commerce, incluindo design de APIs e integrações com ERPs e outros sistemas. Se você precisa de um endpoint construído ou revisado, fale comigo.
Artigos relacionados
Extension attributes no Magento 2: dados em pedidos e APIs
Adicione dados customizados a entidades do Magento 2 com extension attributes: extension_attributes.xml, plugins de repository, storage, saída REST e armadilhas comuns.
Padrões de integração Magento 2 com ERP que se sustentam
Padrões de integração do Magento 2 com ERP: dono dos dados, arquivos em lote, REST e bulk API assíncrona, exportação de pedidos por queue, middleware e as falhas a prever.
Autenticação na REST API do Magento 2: admin, customer e integration
Como funciona a autenticação na REST API do Magento 2: tokens de admin e customer, integrations, OAuth, bearer, 2FA, validade dos tokens e padrões seguros.
Magento 2: tipos de cache, clean, flush e cache type próprio
O que cada cache type do Magento 2 guarda, a diferença real entre cache:clean e cache:flush, caches invalidados e como criar seu próprio cache type.