Кращий підхід для створення REST API endpoint

Всім привіт!
У цій статті ми розглянемо приклад REST API endpoint у Salesforce з використанням єдиного API Gateway.

Що таке Salesforce REST API?

Salesforce REST API дає можливість отримати доступ до даних Salesforce без фактичного інтерфейсу користувача. REST API Salesforce дозволяє інтегрувати Salesforce зі сторонніми програмами та сервісами. Salesforce REST API забезпечує простий і надійний інтерфейс з архітектурою RESTful. Інструменти REST API Salesforce дозволяють маніпулювати даними в Salesforce (CRUD-операції). Це робиться шляхом надсилання запитів HTTP до endpoint-у у Salesforce.

За допомогою Salesforce REST API ми можемо отримувати доступ до записів, результатів запитів SOQL/SOSL, метаданих тощо.

Для використання REST API Salesforce для доступу до даних Salesforce необхідно мати організацію Salesforce з доступом до API (API access). А також дозвіл користувача з увімкненим API (API Enabled).

Salesforce надає доступ до API у наступних версіях:

  • Professional Edition
  • Performance Edition
  • Enterprise Edition
  • Unlimited Edition
  • Developer Edition

Що таке API Gateways?

Архітектурне рішення gateway API служить єдиною точкою входу для конкретних клієнтів або програм, які хочуть отримати доступ до вашого API. Цей gateway може визначити, що саме хоче конкретний клієнт АРІ, а потім спрямувати їх у відповідне місце, або повернути сформовані данні Salesforce у респонсі.

gateway АРІ допомагають масштабувати та ефективно керувати трафіком АРІ та можуть бути особливо корисними, якщо ви використовуєте більше одного мікросервісу.

Побудова архітектури ендпоінту з використанням єдиного API Gateway для безлічі веб-додатків та мікросервісів є важливим фактором для повторного використання компонентів, коду та оптимізації витрат.

Створення endpoint у Salesforce

Ми будемо створювати endpoint з дотримуванням архітектури API getaway.

Для початку створімо Apex class, де ми оголосимо наш endpoint та додамо невелику відповідь на звернення до цього ендпоінту.

Для цього нам потрібно використати додатковий атрибут для класу @RestResource, а також обов’язково зробити клас з модифікатором доступу global.

Далі створюємо метод також з додатковим атрибутом метода HTTP запиту (наприклад, @HttpGet чи @HttpPost) та модифікатором доступу global. Пам’ятайте, у кожному такому класі може бути декілька методів з атрибутами методів HTTP, але у кожний такий атрибут може бути використаний лише для одного методу.

@RestResource(UrlMapping = '/endpoint')
global class CustomEndpoint {
    @HttpGet
    global static void getData() {
        RestContext.response.addHeader('Content-Type', 'application/json');
        RestContext.response.responseBody = Blob.valueOf('{ "message" : "Hello world!"}');
        RestContext.response.statusCode = 200;
    }
}

Після чого ми можемо за допомогою https://workbench.developerforce.com/ надіслати рiквест до нашого SF.

Авторизуємось у нашій Salesforce організації, повертаємось до workbench та йдемо до розділу REST Explorer

Обираємо HTTP метод GET, змінюємо URL на /service/apexrest/endpoint (де /endpoint це наш обраний RestResource(UrlMapping)) та тиснемо Execute.

Ми бачимо нашу відповідь, яку створили у класі CustomEndpoint . Нічого складного :slight_smile:

На практиці ж ми можемо користуватись великою кількістю різних запитів. Вони можуть бути як однотипні, так і дуже різні. Але створювати для кожного типу виклику свій ендпоінт, що тягне за собою створення додаткового класу APEX, не є гарною практикою. Тому я хочу освітити створення ендпоінту та використання DTO для обробки викликів сторонніх сервісів.

DTO (Data Transfer Object) - один з шаблонів проєктування, використовується для передачі даних між підсистемами, не повинен містити в собі якоїсь поведінки.
В нашому прикладі ми будемо використовувати DTO для передачі структурованих даних в Request Body до нашого endpoint, а у Salesforce ми будемо створювати об’єкт на основі структури DTO, який буде утримувати в собі дані з ріквесту. Це нам допоможе легко та зручно використовувати їх.

Для цього нам необхідно додати логіку до нашого класу CustomEndpoint та створити ще декілька додаткових класів. Усе послідовно.

По-перше, створюємо простий DTO для отримання імені виклику нашому endpoint.

public class EndpointTypeDTO {
	 public String name {get; set;}
}

В нашому першому запити ми використовували тип запиту @HttpGet, тому що він не містить у собі Request Body, і для прикладу цього було достатньо. Але далі нам потрібно змінити тип запиту на @HttpPost, щоб ми мали змогу надсилати додаткову інформацію, використовуючи Request Body. Та для прикладу додаємо перевірку імені запиту та просту перевірку на помилки у JSON.

@RestResource(UrlMapping = '/endpoint')
global class CustomEndpoint {
    public static final String REQUEST_NAME_A = 'api_request_a';
    public static final String REQUEST_NAME_B = 'api_request_b';
    
    @HttpPost
    global static void getData() {
		RestRequest req = RestContext.request;
        EndpointTypeDTO type;
        try {
            type = (EndpointTypeDTO) JSON.deserialize(req.requestBody.toString(), EndpointTypeDTO.class);
            
            if (type.name == REQUEST_NAME_A) { 
                RestContext.response.responseBody = Blob.valueOf('{ "message" : "Hello world!"}');
            } else if (type.name == REQUEST_NAME_B) {
                RestContext.response.responseBody = Blob.valueOf('{ "message" : "Have a nice day!"}');
            }       
        } catch (JSONException e){
           setExceptionResponse(e); 
        }
    }
    
    private static void setExceptionResponse(Exception e){
        RestContext.response.responseBody = Blob.valueOf('{ "message" : "' + e + '"}');
        RestContext.response.statusCode = 500;
    }
}

Давайте протестуємо що в нас вийшло за допомогою надсилання різного Request Body до нашого ендпоінту.

  1. Надсилаємо request body з незаповненим JSON {“”}
  2. Надсилаємо request body {“name” : “api_request_a”}
  3. Надсилаємо request body {“name” : “api_request_b”}

Це перший крок до створення ендпоінту. Далі ми можемо надсилати різні дані до нашого endpointy за допомогою Request Body та поля name. Salesforce за допомогою Apex буде розуміти, що потрібно робити з цими даними.

Продовжимо.
Створюємо інтерфейс, який ми повинні будемо імплементувати до наших структур request body. Додамо в нього лише один метод для прикладу.

public interface EndpointDTO {
    String getRecordID();
}

Створюємо DTO для наших типів запитів з імплементацією створеного інтерфейсу.

public class RequestAStructure implements EndpointDTO {
    public RequestA data {get; set;}
    
    public String getRecordId(){
        String recordId;
        if (data != null) {
            recordId = data.accountId;
        }
        return recordId;
    }
}
public class RequestBStructure implements EndpointDTO {
    public RequestB data {get; set;}
    
    public String getRecordId(){
        String recordId;
        if (data != null) {
            recordId = data.accountId;
        }
        return recordId;
    }
}

В даному прикладі вони відрізняються лише класом для змінної data, що нам і треба.

Та створюємо самі класи data й додамо декілька полів для використання їх в APEX.

public class RequestA {
    public String accountId {get; set;}
    public String name {get; set;}
}
public class RequestB {
    public String accountId {get; set;}
    public String description {get; set;}
}

Створюємо Апекс клас хелпер для ендпоинту EndpointHelper. В нього додаємо два статичних методи, які ми будемо викликати із кастомного ендпоінту (CustomEndpoint).

public class EndpointHelper {
    public static void upsertAccount(RequestAStructure body) {
        Account acc = new Account();
        acc.Id = body.data.accountId;
        acc.Name = body.data.Name;
        
        upsert acc;
        
        if (body.data.accountId == '' || body.data.accountId == null) {
        	RestContext.response.responseBody = Blob.valueOf('{ "message" : "Add new Account. Id: ' + acc.Id 
                                                             + ', Name: ' + acc.Name + '"}');   
        } else {
            RestContext.response.responseBody = Blob.valueOf('{ "message" : "Updated Account. Id: ' + acc.Id 
                                                             + ', Name: ' + acc.Name + '"}');    
        }
        
    }
    
    public static void setAccountDescription(RequestBStructure body) {
        if (body.data.accountId != null || body.data.accountId != '') {
            Account acc = new Account();
        	acc.Id = body.data.accountId;
        	acc.Description = body.data.Description;
        
        	update acc;
            
            acc = [SELECT Id, Name, Description FROM Account WHERE Id = :acc.Id];
            
        	RestContext.response.responseBody = Blob.valueOf('{ "message" : "Added new Description to Account. Id: ' + acc.Id 
                                                             + ', Name: ' + acc.Name 
                                                             + ', Description: ' + acc.Description + '"}');
        }
    }
}

І, нарешті, ми коригуємо CustomEndpoint. Додаємо виклики статичних методів в залежності від отриманого типу ріквесту.

@RestResource(UrlMapping = '/endpoint')
global class CustomEndpoint {
    public static final String REQUEST_NAME_A = 'api_request_a';
    public static final String REQUEST_NAME_B = 'api_request_b';
    
    @HttpPost
    global static void getData() {
		RestRequest req = RestContext.request;
        EndpointTypeDTO type;
        EndpointDTO requestData;
        try {
            type = (EndpointTypeDTO) JSON.deserialize(req.requestBody.toString(), EndpointTypeDTO.class);
            
            if (type.name == REQUEST_NAME_A) { 
                requestData = (RequestAStructure) JSON.deserialize(req.requestBody.toString(), RequestAStructure.class);
                EndpointHelper.upsertAccount((RequestAStructure) requestData);
            } else if (type.name == REQUEST_NAME_B) {
                requestData = (RequestBStructure) JSON.deserialize(req.requestBody.toString(), RequestBStructure.class);
                EndpointHelper.setAccountDescription((RequestBStructure) requestData);
            }       
        } catch (JSONException e){
           setExceptionResponse(e); 
        }
    }
    
    private static void setExceptionResponse(Exception e){
        RestContext.response.responseBody = Blob.valueOf('{ "message" : "' + e + '"}');
        RestContext.response.statusCode = 500;
    }
}

Протестуємо те що в нас вийшло.

  1. Надсилаємо на endpoint request body
{
    "name":"api_request_a",
    "data":{
        "name":"Test"
    }
}

отримуємо наступний результат

Ми бачимо, що СФ створило новий аккаунт та повернуло нам у відповідь його ім’я та ІД. У СФ ми можемо побачити також наш новий аккаунт.

  1. Додамо до нашого request body поле з accountId що ми отримали з попередньої відповіді та змінимо ім’я.
{
    "name":"api_request_a",
    "data":{
        "name":"Old Test",
        "accountId": "0017Q00000TxL9VQAV"
    }
}

відповідь:

Я гадаю, цей крок зрозумілий: ми явно вказали ID аккаунту якому змінити ім’я, і СФ оновив запис не створюючи новий аккаунт.

  1. Переходимо до наступного типу ріквеста, що ми створили. Ми надішлемо на ендпоінт інший request body, в якому вкажемо лише ID Аккаунту та Дескріпшн який ми хочемо додати.
{
    "name":"api_request_b",
    "data":{
        "accountId": "0017Q00000TxL9VQAV",
        "description":"Some descriptions of the account"
    }
}

Ми отримаємо відповідь, де є інформація з аккаунту, який ми оновили.

Це, на мою думку, дуже простий та зрозумілий приклад основи створення REST API endpoint у Salesforce. Але він далеко не повний, тому що ми не освітили ще як мінімум дві великі теми.

Security - як проходить аутентифікація до Salesforce, які є для цього технології та можливості.

DataValidation - треба додавати велику кількість перевірок на отримання валідних даних, та очікувану обробку цих помилок.

Висновок: Такий підхід дає нам змогу поєднувати ендпоінти за призначенням, виконувати різні дії з даними з використанням лише одного ендпоінту.

:heavy_plus_sign: Менша кількість створених APEX класів
:heavy_plus_sign: Зменшення кількості коду та його повторне використання
:heavy_minus_sign: Для девелопера з малим стажем треба більше часу аби розібратись

Якщо цей приклад для вас був цікавий, ставте лайки. У наступних статтях спробую розповісти про Security та різномаїття авторизацій, які валідації даних мають бути, та які відповіді з помилками доречно відправляти, а які не треба з міркувань безпеки.

9 Likes