This article describes general guidance to implement a CRUD API for an RMB based module (mod-inventory-storage is used as an sample module). Usually, such API is created for so-called reference data entities - supporting data, that has a list of predefined values, but can be updated/added by institutions in future (e.g. instance statuses, holdings record sources, identifier types, patron groups, etc.).
The API development can be expressed in the following steps:
- Model API and domain objects.
- Declare the API in module descriptor.
- Define DB table for the new entity.
- Implement the API logic.
- Apply necessary DB constraints and indexes.
- Prepare integration tests for the new API.
- Create system predefined data for the API.
- Make sure the predefined data is loaded.
0. Example used description
For the guidance let's build CRUD API for instance sources. Instance source defines source of the instance - right now we have only MARC and FOLIO sources, but institutions can introduce some custom sources. An instance source usually have an id, name of the source and source - which defines who has created the record - either folio (it is a system source) or local - if it is added by a user, there is also the metadata property which is a system property that holds some technical info about when the record was created and updated, and the user is who has created and updated record.
1. API and domain objects modeling
FOLIO uses JSON schema to define structure of a domain object, this definition is converted to a java class(es) by a maven plugin automatically. RAML is used to define an API (request/response structure, query parameters, possible response statuses, endpoint names, etc.), it is also used to generate API documentation that is published here.
Usually, a CRUD API needs following endpoints:
- POST endpoint with entity as request - to create an entity, it has
- GET by id endpoint;
- GET collection of entities by a CQL query supporting pagination;
- PUT endpoint - to update an entity by id;
- DELETE endpoint - to remove an entity by id.
We usually use following URI structure, within core func modules: /{domain-name}-storage/{domain-name} - e.g. /instance-sources-storage/instance-sources. In this case, the API endpoints for the instance-sources API will have following URIs:
- POST /instance-sources-storage/instance-sources
- GET /instance-sources-storage/instance-sources/{id}
- GET /instance-sources-storage/instance-sources?query=<a-query>&limit=<a-limit>&offset=<an-offset>
- PUT /instance-sources-storage/instance-sources/{id}
- DELETE /instance-sources-storage/instance-sources/{id}
Let's define JSON schema for the instance-source.
{ "$schema": "http://json-schema.org/draft-04/schema#", "description": "...", "type": "object", "properties": { "id": { "type": "string", "description": "..." }, "name": { "type": "string", "description": "..." }, "source": { "type": "string", "description": "..." }, "metadata": { "type": "object", "$ref": "raml-util/schemas/metadata.schema", "readonly": true } }, "additionalProperties": false, "required": [ "name", "source" ] }
Please pay attention that the metadata property is read-only, there is also additionalProperties - it tells that no other properties are allowed, otherwise the validation is failing. We have added name and source to the required list - it means that validation is also fails if those properties are missing in request.
Create a collection schema for the domain - which will be used as response for get by CQL endpoint. You can use an existing domain as an example, the schema basically defines two properties - array of the domain objects and totalRecords - an integer saying how many records matches the criteria.
Next, we need to define RAML for the API.
#%RAML 1.0 title: Instance Sources API version: v1.0 protocols: [ HTTP, HTTPS ] baseUri: http://localhost documentation: - title: Instance Sources API content: This documents the API calls that can be made to query and manage instance sources types: instanceSource: !include instance-source.json #name of the file with domain schema instanceSources: !include instance-sources.json #name of the file for get by query response errors: !include raml-util/schemas/errors.schema traits: pageable: !include raml-util/traits/pageable.raml searchable: !include raml-util/traits/searchable.raml language: !include raml-util/traits/language.raml validate: !include raml-util/traits/validation.raml resourceTypes: collection: !include raml-util/rtypes/collection.raml collection-item: !include raml-util/rtypes/item-collection.raml /instance-sources-storage/instance-sources: # the common part of the URI type: collection: exampleCollection: !include examples/<example-file-for-get-many-response>.json # replace with actual name exampleItem: !include examples/<example-file-for-single-item>.json # replace with actual name of single item example schemaCollection: instanceSources # the one we defined in the traits section schemaItem: instanceSource # the one we defined in the traits section get: is: [ searchable: {description: "with valid searchable fields", example: "name=aaa"}, pageable ] description: Return a list of instance sources post: description: Create a new instance source is: [validate] /{id}: description: Pass in the instance source id type: collection-item: # This is predefined type, it will add GET by id, PUT by id, DELETE by id exampleItem: !include examples/<example-file-for-single-item>.json # replace with actual name schema: instanceSource #the type from traits section
As you can see, there are already some predefined