Belajar menilai seberapa sempurna rancangan REST API

Membuat API berbasis REST pada dasarnya adalah sesuatu yang sangat mudah: buat sebuah method di controller dan kembalikan hasil berupa JSON. Selesai! Yup, cukup sampai disini bagi programmer yang sedang belajar atau membuat proyek jangka pendek yang tidak ingin berkembang. Pada tulisan kali ini, saya akan melihat dari sisi arsitektur, dimana REST API tersebut akan dipakai dalam jangka lama dan mungkin akan dipanggil oleh klien baru yang belum terpikirkan saat ini.

Salah satu cara untuk menilai implementasi REST API adalah dengan membandingkannya dengan Richardson Maturity Model. Gambar berikut ini memperlihatkan perkembangan REST menurut model tersebut:

Richardson Maturity Model

Richardson Maturity Model

Pada Level 0, REST API dianggap sebagai API jarak jauh yang dipanggil melalui Web (Http) dan mengembalikan JSON. Tidak lebih dari sekedar pengganti function atau method (yang dikerjakan jarak jauh)! Sebagai contoh, API berikut ini masuk dalam kategori Level 0:

POST http://api.server.com/buatProduk
{
   nama: "produk1",
   harga: 1000
}

POST http://api.server.com/hapusProduk
{
   id: 1
}

POST http://api.server.com/daftarPelanggan
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

POST http://api.server.com/hapusPelanggan
{
   id: 1
}

Pada Level 1, terdapat konsep resource. Setiap API merupakan operasi yang akan melakukan sesuatu pada resource yang ada. Sebagai contoh, API berikut ini termasuk dalam kategori Level 1:

POST http://api.server.com/produk/buat
{
   nama: "produk1",
   harga: 1000
}

POST http://api.server.com/produk/hapus
{
   id: 1
}

POST http://api.server.com/pelanggan/daftar
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

POST http://api.server.com/pelanggan/hapus
{
   id: 1
}

Pada contoh di atas, setiap API masih dipanggil dengan request POST yang populer dari dulu. Biasanya, GET dipakai bila kita tidak ingin mengirim data tambahan seperti JSON. Bila ingin mengirimkan JSON atau mengirim data dalam jumlah besar, POST akan digunakan. Padahal selain GET dan POST, HTTP juga memiliki request method lainnya seperti HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT, dan PATCH.

Sebuah request yang sukses akan mengembalikan respon 200. Selain respon 200, HTTP juga memiliki definisi response status code yang lain seperti 500 untuk menandakan kesalahan, 201 untuk menandakan resource berhasil dibuat, dan sebagainya.

Pada Level 2, REST API akan memanfaatkan HTTP request method dan response status code. Sebagai contoh, API berikut ini masuk dalam kategori Level 2:

POST http://api.server.com/produk
{
   nama: "produk1",
   harga: 1000
}

PUT http://api.server.com/produk/1
{
   nama: "produk1 dengan nama baru",
   harga: 2000
}

DELETE http://api.server.com/produk/1

POST http://api.server.com/pelanggan
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

DELETE http://api.server.com/pelanggan/1

Pada Level 3, REST API akan menggunakan HATEOAS. Sebagai contoh, saya membuat REST API yang mendukung HATEOAS di artikel Membuat RESTFul Web Service Dengan Spring Data REST. Kenapa memakai HATEOAS? Beberapa keuntungan yang ditawarkannya antara lain:

  1. Klien tidak perlu men-hardcode link untuk melakukan operasi terhadap resource yang diterima.  Dengan demikian, perubahan nama atau lokasi API tidak menimbulkan perubahan di kode program klien.
  2. Klien dapat mengetahui operasi apa yang diperbolehkan dan tidak diperbolehkan terhadap resource.   Sebagai contoh, pada UI yang berisi daftar produk, bila tidak ada link untuk menghapus produk yang dikembalikan bersamaan dengan produk tersebut (misalnya karena user yang login tidak berhak untuk operasi tersebut), maka saya bisa menyembunyikan tombol hapus.

Sayangnya, hingga saat ini tidak ada implementasi yang standar untuk HATEOAS.  Salah satu yang menjanjikan adalah HAL (Hypertext Application Language).  Daftar library yang mengimplementasikan HAL dapat dijumpai di https://github.com/mikekelly/hal_specification/wiki/Libraries.

REST API dari Paypal dan Netflix adalah contoh API yang sudah mendukung HATEOAS.  Sementara itu, kebanyakan penyedia REST API publik lainnya hanya berada pada Level 2 karena mereka berfokus pada kemudahan penggunaan (sementara HATEOAS tidak begitu populer).   Perlu diingat ini bahwa mereka adalah API publik yang ditujukan untuk dikonsumsi oleh developer luar dari berbagai kalangan.  Fokus ini tentu akan berbeda, misalnya, pada saat merancang arsitektur microservices untuk dipakai dalam perusahaan.

Membuat RESTful Web Service Dengan Spring Data REST

Pada tulisan tentang web service sebelumnya, saya selalu menggunakan SOAP. Selain memakai SOAP, solusi yang kini populer adalah REST. Yup! Saya sering berpikir SOA telah terkubur karena mulai jarang diperbicarakan. Tapi ternyata masih ada buku baru Thomas Erl yang diterbitkan pada Agustus 2012 yang berjudul “SOA with REST: Principles, Patterns & Constraints for Building Enterprise Solutions with REST“.

Saya tidak akan membicarakan teori disini, tapi apa kelebihan REST? Bila SOAP adalah “early-binding” (bayangkan sebuah variabel dengan tipe data yang ketat dan sudah pasti!) maka REST adalah “late-binding” (bayangkan variabel generik tanpa tipe data seperti di JavaScript). REST lebih ringan karena bisa dipakai cukup dengan memanggil request HTTP. REST juga tidak perlu memakai WSDL untuk mengetahui kontrak servis! Loh, jika tidak ada WSDL, lalu bagaimana mengetahui layanan apa yang disediakan? REST mensyaratkan penggunaan URI dan Request Method yang harus dipatuhi. Sebagai contoh:

REST pada dasarnya adalah mengakses HTTP layaknya browsing.  Dengan demikian, tidak dibutuhkan “kode program” khusus.  Semua bahasa yang mendukung akses HTTP dapat memakai REST.  Yang membedakan REST adalah pada REST terdapat peraturan-peraturan yang harus “dipatuhi” sehingga komunikasi bisa berjalan lancar.

Walaupun demikian, REST bukanlah protokol seperti SOAP, sehingga “peraturan”-nya boleh dilanggar.  Hal ini dapat membingungkan client yang memakai.  Yup! Kadang-kadang sulit menjelaskan pada mahasiswa yang berharap menemukan kode program atau teknik baru yang mutakhir;  REST berhubungan dengan disiplin dan ‘manajemen’.  Mahasiswa yang berfokus pada membuat sistem untuk skripsi lalu meninggalkannya begitu saja setelah lulus akan sulit merasakan manfaatnya.

Saya akan mulai membuat RESTful web service dengan memakai Spring Data REST. Saya mulai dengan membuat sebuah proyek baru dengan memilih File, New, Spring Template Project di Spring Tool Suite (STS).  Pada dialog yang muncul, saya memilih Spring MVC Project dan men-klik tombol Next.  Saya mengisi nama proyek dengan rest-api dan lokasi package dengan com.jocki.rest.  Lalu, saya men-klik tombol Finish.

Setelah proyek selesai dibuat, saya me-double click file pom.xml untuk memastikan bahwa properties org.springframework-version minimal adalah versi 3.0. Selain itu, saya akan menambahkan dependency berikut ini:

  • org.hibernate : hibernate-entitymanager : 3.6.0.Final (sebagai JPA provider)
  • org.springframework.data : spring-data-jpa : 1.1.0.RELEASE
  • org.springframework : spring-tx : ${org.springframework-version}
  • org.springframework : spring-orm : ${org.springframework-version}
  • org.springframework.data : spring-data-rest-webmvc : 1.0.0.RELEASE
  • com.h2database : h2 : 1.3.167 (sebagai database in-memory)

Pada Package Explorer, saya men-klik kanan folder src/main/java, men-klik kanan dan memilih menu New, Class. Saya mengisi Package dengan com.jocki.domain, dan mengisi Name dengan Buku. Class ini akan mewakili domain class saya, dimana isi kode programnya adalah:

package com.jocki.domain;

import java.io.Serializable;

import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Cacheable
public class Buku implements Serializable {

	private static final long serialVersionUID = 9099989372502423316L;

	@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
	private Long id;

	private String judul;

	private String isbn;

	private Integer harga;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getJudul() {
		return judul;
	}

	public void setJudul(String judul) {
		this.judul = judul;
	}

	public String getIsbn() {
		return isbn;
	}

	public void setIsbn(String isbn) {
		this.isbn = isbn;
	}

	public Integer getHarga() {
		return harga;
	}

	public void setHarga(Integer harga) {
		this.harga = harga;
	}

}

Untuk melakukan operasi persistensi (CRUD ke database embedded H2), saya akan menggunakan Spring Data JPA. Untuk itu, saya men-klik kanan pada folder src/main/java, kemudian memilih New, Interface. Pada Package, saya mengisi com.jocki.repository dan pada Name, saya mengisi BukuRepository. Kode program untuk interface tersebut akan terlihat seperti berikut ini:

package com.jocki.respository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.jocki.domain.Buku;

public interface BukuRepository extends JpaRepository<Buku, Long>{

}

Saya akan mulai dengan membuka folder src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml. Lalu, saya akan menambahkan definisi bean berikut ini:

<beans:bean class="org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration" />

Bean tersebut dibutuhkan untuk men-export seluruh repository saya secara otomatis sehingga operasi persistensi data pada domain object Buku dapat diakses melalui REST.

Berikutnya, saya perlu menyiapkan konfigurasi persistensi data. Saya akan mengubah file src/main/webapp/WEB-INF/spring/root-context.xml. Saya mendouble-click file ini, kemudian memilih tab Namespaces dan memastikan bahwa saya memberi tanda centang pada beans, context, jdbc, jpa, dan tx. Lalu saya menambahkan definisi berikut ini pada file root-context.xml:

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
  <property name="entityManagerFactory" ref="emf" /> 
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="jpaVendorAdapter"> 
     <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
  </property> 
  <property name="packagesToScan" value="com.jocki.domain" /> 
  <property name="jpaProperties">
    <props>
      <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
      <prop key="hibernate.hbm2ddl.auto">create-drop</prop>
      <prop key="hibernate.id.new_generator_mappings">true</prop>
    </props>
  </property>
</bean>
<context:annotation-config />
<context:component-scan base-package="com.jocki" />
<jpa:repositories base-package="com.jocki.repository" />
<jdbc:embedded-database id="dataSource" type="H2" />

Setelah ini, saya tinggal menjalankan tc Server.  Caranya adalah dengan men-klik kanan pada nama proyek, memilih menu Run As, Run on server.  Pada dialog yang muncul, saya memilih salah satu server yang tersedia, misalnya tc Server v2.7, lalu men-klik tombol Finish. Setelah server selesai dijalankan, browser secara otomatis akan terbuka pada URL http://localhost:8080/rest.

Spring Data REST mendukung HATEOAS (Hypermedia as the Engine of Application State) dimana terdapat links dari sebuah resource ke resource lainnya secara konsisten.  Yang muncul pertama kali di URL tersebut adalah seluruh daftar resource yang disediakan oleh Spring Data RESTseperti berikut ini:

{
  "links" : [ {
    "rel" : "buku",
    "href" : "http://localhost:8080/rest/buku"
  } ],
  "content" : [ ]
}

REST tidak mensyaratkan penggunaan format tertentu untuk konten.  Client dapat memilih format dengan menyertakan header Content-Type yang diinginkan seperti application/json atau application/xml.  Tapi untuk saat ini, Spring Data REST secara bawaan (tanpa perubahan!) hanya akan mengembalikan format application/json.

Untuk menguji REST, saya akan memakai tool curl.

Untuk melihat daftar seluruh resource “buku” yang ada di database,  saya perlu memberikan method GET pada URL http://localhost:8080/rest/buku seperti yang terlihat pada gambar berikut ini:

Memakai REST untuk melihat seluruh "buku" yang tersedia.

Operasi REST untuk melihat seluruh “buku” yang tersedia.

Karena saya menurunkan interface BukuRepository dari JpaRepository, maka secara otomatis saya memiliki fitur penghalamanan (paging).

Untuk melihat method apa saja yang didukung di RESTful web services, saya perlu memberikan request HTTP dengan method OPTIONS seperti yang terlihat pada gambar berikut ini:

Melihat operasi yang didukung oleh RESTful API

Melihat operasi yang didukung oleh RESTful API

Sekarang, saya akan menambah sebuah buku baru.  Untuk itu saya meng-hit URL dengan method POST seperti pada gambar berikut ini:

Membuat resource baru dengan REST

Membuat resource baru dengan REST

Saya terpaksa memberikan perintah tersebut di console Bash karena selalu gagal di console Windows.  Sekarang, bila saya melihat daftar “Buku” yang ada, saya akan menemukan hasil seperti pada gambar berikut ini:

Melihat seluruh "Buku" yang ada dengan REST

Melihat seluruh “Buku” yang ada dengan REST

Untuk menghapus buku dengan id 1, saya bisa melakukan request HTTP dengan method DELETE, seperti yang terlihat pada gambar berikut ini:

Menghapus resource dengan REST

Menghapus resource dengan REST

Perhatikan bahwa respon yang dikembalikan adalah 204 No Content, bukan 404 Not Found dan sebagainya.  Respon tersebut menunjukkan bahwa operasi hapus telah berhasil dilakukan dan tidak ada sesuatu yang perlu dikembalikan pada client.

Bagi yang ingin memakai tools berbasis GUI, bisa mencoba plugin Firefox seperti plugin RESTClient.  Tools ini mendukung authentication dengan OAuth2, sebuah pengamanan yang umum dipakai untuk RESTful web services.  Berikut ini adalah contoh tampilan plugin RESTClient di Firefox:

Tampilan plugin RESTClient di Firefox

Tampilan plugin RESTClient di Firefox

Pada prakteknya, tentu saja operasi REST tidak dipanggil melalui cURL atau plugin Firefox, melainkan oleh sistem lain atau aplikasi lain.  Hampir semua bahasa pemograman bisa melakukan request HTTP secara bawaan.   Beberapa bahkan sudah menyediakan akses REST secara mudah, misalnya Jersey di Java atau Zend_Rest_Client di PHP (yang ini  hanya mendukung XML dan banyak “melanggar” prinsip REST).  Seorang mahasiswa yang baru belajar mungkin akan bertanya, kenapa harus selalu melibatkan sistem lain?  Karena pada dasarnya web service dan SOA adalah mengenai komunikasi antar-sistem atau antar-program!  Penerapan pada sebuah sistem tunggal tanpa komunikasi cukup disebut SOP (Service Oriented Programming).

Spring Data REST memang mempermudah meng-export repository menjadi RESTful Web Services.  Tetapi RESTful Web Services tidak hanya berisi operasi CRUD saja, melainkan juga services (business logic).  Untuk mendapatkan kendali yang lebih penuh, RESTful API dapat dibuat dari Spring Web MVC controller yang telah dilengkapi annotation yang mendukung REST.

Memakai MessageConverter di Spring 3.1 Untuk Web Services REST

Salah satu fitur menarik yang diperkenalkan oleh Spring Framework 3.1 adalah message converter yang dalam bentuk tag <mvc:message-converters>.  Dengan message converter, saya bisa membuat representasi  object di aplikasi saya dengan mudah dalam bentuk JSON dan XML secara otomatis.   Untuk mendukung konversi objek ke/dari JSON, saya memakai Jackson JSON library (http://jackson.codehaus.org).  Sementara untuk konversi objek ke/dari XML, saya memakai Castor (http://castor.codehaus.org).

Saya mulai dengan membuat sebuah proyek baru di STS, dengan memakai template Spring MVC Project.   Sebelum mulai membuat kode program, saya menambahkan dependencies Maven ke proyek saya seperti yang terlihat pada gambar berikut ini:

Dependencies Maven

Dependencies Maven

Kemudian saya membuat sebuah class dengan nama Mahasiswa di package co.id.jocki.domain.  Isi dari class Mahasiswa adalah:

package co.id.jocki.domain;

import java.io.Serializable;

public class Mahasiswa implements Serializable {

	private static final long serialVersionUID = 225855015823197676L;

	private String nim;
	private String nama;
	private int usia;

	public String getNim() {
		return nim;
	}
	public void setNim(String nim) {
		this.nim = nim;
	}
	public String getNama() {
		return nama;
	}
	public void setNama(String nama) {
		this.nama = nama;
	}
	public int getUsia() {
		return usia;
	}
	public void setUsia(int usia) {
		this.usia = usia;
	}	

}

Lalu, pada package co.id.jocki, saya membuat sebuah class bernama LatihanController.  Isi dari class tersebut adalah:

package co.id.jocki;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import co.id.jocki.domain.Mahasiswa;

@Controller
@RequestMapping(value="/mahasiswa")
public class LatihanController {

	private Mahasiswa mahasiswa;

	@RequestMapping(value="/listdata", method=RequestMethod.GET)
	@ResponseBody
	public Mahasiswa listData() {
		if (mahasiswa==null) {
			mahasiswa = new Mahasiswa();
			mahasiswa.setNama("Makdalena Hendry");
			mahasiswa.setNim("99999999");
			mahasiswa.setUsia(21);
		}
		return mahasiswa;
	}

}

Pada kasus nyata, tentu saja isi controller tidak sesederhana ini (misalnya masih ada get, delete, update, dsb).  Object yang ada juga tidak dibuat disini, melainkan seharusnya diambil dari medium penyimpanan (misalnya database).

Yang menarik disini adalah saya  tidak melakukan proses transformasi ke JSON ataupun XML secara manual.  Saya juga tidak memanggil sebuah fungsi ajaib.  Saya hanya mengembalikan sebuah objek mahasiswa seperti biasanya layaknya kode program standard.

Lalu bagaimana konversi bisa dilakukan?  Karena saya memberitahukannya secara deklaratif (tanpa kode program) dengan mengedit file servlet-context.xml.  File ini dapat ditemukan di lokasi src/main/webapp/WEB-INF/spring/appServlet.  Saya mengubah file tersebut sehingga isinya menjadi:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

  <mvc:annotation-driven>
    <mvc:message-converters>
      <beans:bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
      <beans:bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
        <beans:property name="marshaller" ref="castorMarshaller"/>
        <beans:property name="unmarshaller" ref="castorMarshaller"/>
      </beans:bean>
    </mvc:message-converters>
  </mvc:annotation-driven>

  <beans:bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller">
    <beans:property name="mappingLocation" value="classpath:oxm-mapping.xml"/>
  </beans:bean>

  <context:component-scan base-package="co.id.jocki" />

</beans:beans>

Rahasianya terletak di <mvc:message-converters> dimana saya mendeklarasikan bean dari class MappingJacksonHttpMessageConverter dan class MarshallingHttpMessageConverter.  Khusus untuk yang XML, saya perlu membuat file oxm-mapping.xml (nama yang sama seperti di property mappingLocation di bean castorMarshaller.

Saya akan membuat file oxm-mapping.xml ini di folder src/main/resources.  Isi file tersebut menentukan bagaimana memetakan sebuah class Java ke XML (dan sebaliknya) seperti yang terlihat di berikut ini:

<?xml version="1.0" encoding="UTF-8"?>
<mapping xmlns="http://castor.exolab.org/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://castor.exolab.org/ http://castor.org/mapping.xsd">

  <class name="co.id.jocki.domain.Mahasiswa">
    <map-to xml="mahasiswa" ns-uri="http://jockihendry.com/mahasiswa/"
       ns-prefix="mahasiswa"/>

    <field name="nim" type="string" >
      <bind-xml auto-naming="deriveByField" node="element"/>
    </field>

    <field name="nama" type="string">
      <bind-xml auto-naming="deriveByField" node="element" />
    </field>

    <field name="usia" type="integer">
      <bind-xml auto-naming="deriveByField" node="element" />
    </field>
  </class>

</mapping>

Setelah ini, saya  menjalankan tc Server untuk menguji web service REST tersebut.

Untuk melakukan pengujian, saya akan menggunakan cURL (http://curl.haxx.se), sebuah tools command-line yang bisa dipakai untuk browsing berbasis teks.  Karena saya pernah meng-install Zend Studio, tools tersebut secara otomatis sudah ada dan siap dipakai.

Saya mulai dengan memanggil halaman http://localhost:8080/latihan-rest/mahasiswa/listdata.  Pada kode program, terlihat controller hanya mengembalikan sebuah objek mahasiswa.  Tetapi saya menginginkan kembalian berupa JSON.  Dan, server saya ternyata sudah mendukungnya seperti yang terlihat di tampilan berikut:

Komunikasi REST dengan JSON

Komunikasi REST dengan JSON

Lalu, kali ini saya menginginkan kembalian berupa XML.  Dan sekali lagi, server saya secara otomatis sudah mendukungnya seperti yang terlihat di gambar berikut:

Komunikasi REST dengan XML

Komunikasi REST dengan XML