Mastering Data Consistency in POST API Development

Whenever creating an endpoint for a POST API method, it is important to consider what data from the incoming request will be stored. What’s more, data integrity at all levels of the application remains very important. So how can this be effectively achieved? And what are the consequences of overlooking certain aspects? This article aims to discuss some of the potential errors and show how they can be avoided by skillful use of the available libraries and tools.

How to maintain data integrity?

I’m pretty sure none of us would want to work with a system that doesn’t maintain data integrity. We want each person recorded in the database to have a first and last name, and the city address. We are making an assumption, that if we save the company’s data, the information about the industry in which the company operates will not disappear. When we create a dictionary of available professions, we expect it not to consist of empty or duplicate values. When working on the API, it is also worth taking care of the documentation that will specify what data a particular endpoint expects, in what format, and which fields are required.

Based on the above, emphasis should be placed on the following sub-points:
1. The database model should specify the required fields. Especially when it comes to foreign keys and key data.
2. Entities should be consistent with the data model
3. DTOs should be entity-consistent. They should not send technical data.
4. Validations should inform the user about potential errors and prevent incorrect records from being created.

How to provide consistency in application?

On the database side, it should be basic annotations. This is mainly NOT NULL and UNIQUE. In addition, tables should contain appropriately defined foreign keys. In applications written with the use of Java, the javax.validation library and the annotations provided by it may be helpful. Below I will briefly present some of the most important of them. To protect against empty fields, we can use one of these annotations:

@NotNull: a constrained CharSequence, Collection, Map, or Array is valid as long as it’s not null, but it can be empty.
@NotEmpty: a constrained CharSequence, Collection, Map, or Array is valid as long as it’s not null, and its size/length is greater than zero.
@NotBlank: a constrained String is valid as long as it’s not null, and the trimmed length is greater than zero.

Can you show me the example?

First and foremost, let’s begin with the creation of a simple model. The primary objective is to fashion a dictionary that houses comprehensive information about various regions. These regions, as entities, possess fundamental attributes such as 'name’ and 'description.’ Initially, the database model will closely resemble the following representation.

CREATE TABLE public.regions (
  id bigint NOT NULL,
  name character varying(32),
  description character varying(512));

Based on the code above, lets can create entity and DTO, which will be schema to communicate by API.

@Entity
@Table(name = "regions")
public class Region {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String name;
  private String description;
}

@Data
@Builder
public class RegionDTO {
  private Long id;
  private String name;
  private String description;
}

After that I will create simple controller which provide POST method to create Region.

@Operation(description = "Create new Region")
@PostMapping
public ResponseEntity<RegionDTO> createRegion(@RequestBody RegionDTO regionRequest) {
  var savedDTO = regionService.createRegion(regionRequest);
  var resourceLocation = URI.create("/api/regions" + savedDTO.getId());
  return ResponseEntity.created(resourceLocation).build();
}

Controller is ready to receive POST requests, and save it into the database. Of course there is a part, where  mappers and service implementation was created, but for the scope of this article it was omitted.
Easily, I can send a POST request to an address and create a new region.

curl --location 'localhost:8080/api/regions' \
--header 'Content-Type: application/json' \
--data 
'{
  "name": "EMEA",
  "description": "Europe, the Middle East and Africa"
}'

Should the field be unique?

Stop here for a moment. If I closely look at a database model, I can see one potential issue. Should I be able to create two records with the same name here? It’s a dictionary, so I expect unique values. That’s a thing which wasn’t mentioned in business analysis, but as a developer providing a solution, I should always stop for a moment and think in which business cases my endpoints will be used. I should know that dictionaries should contain rather unique values as a key. Of course, not always we have an example simple like this, but always we are in a place, where we can discuss with someone responsible for architecture or business site of a project how the provided solution should work. 

Assurance of uniqueness should be provided at all possible levels of our application. On a database level, the column should be labelled with UNIQUE constraint.

CREATE TABLE public.regions (
  id bigint NOT NULL,
  name character varying(32),
  description character varying(512));
ALTER TABLE public.regions ADD CONSTRAINT uq_regions_name UNIQUE (name);

For consistency, a unique annotation should be added on the column in the Entity. At this moment there is no option to validate unique name on DTO level, but it should be also documented to potential API clients. To avoid getting SQL errors in API response, save method in service should be extended to check if the region exists and if yes, then throw an exception. After changes, code will look like this.

-- entity
@Entity
@Table(name = "regions")
public class Region {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(unique=true)
  private String name;
  private String description;
}

-- dto
@Data
@Builder
public class RegionDTO {
  private Long id;
  @Schema(description = "Unique region name")
  private String name;
  private String description;
}

-- service
public RegionDTO createRegion(RegionDTO createRegionDTO) {
  if (regionRepository.findByName(createRegionDTO.getName()).isPresent()) {
      throw new EntityIsPresentException(Message.entityAlreadyExists);
  }
  return regionMapper.asDTO(regionRepository.save(regionMapper.asEntity(createRegionDTO)));
}

After making these changes, I ensure consistency between the API model and the database in terms of a unique name for the Region. I also inform you that it is unique in the documentation compliant with the OpenAPI standard – Swagger.

Should the field be empty?

Let’s take another look at the updated model. The name can be unique. That’s great! But if I think for a moment, I can see that it is possible to set the region name as empty or containing only spaces! Is this the correct behavior? Should I later assign things to an empty region? It probably won’t be the best decision.

As in a case above. Database model should be fixed using NOT NULL label. After that, this change should be reflected in Entity and DTO model. This is where the previously mentioned javax.validation library comes to our aid.

-- database table
CREATE TABLE public.regions (
  id bigint NOT NULL,
  name character varying(32) NOT NULL,
  description character varying(512) NOT NULL);
ALTER TABLE public.regions ADD CONSTRAINT uq_regions_name UNIQUE (name);
-- entity
@Entity
@Table(name = "regions")
public class Region {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @NotBlank
  @Column(unique=true)
  private String name;
  @NotBlank
  private String description;
}

-- dto
@Data
@Builder
public class RegionDTO {
  private Long id;
  @Schema(description = "Unique region name")
  @NotBlank(message = "Region can't be null")
  private String name;
  @NotBlank(message = "Description can't be null")
  private String description;
}

-- controller
@Operation(description = "Create new Region")
@PostMapping
public ResponseEntity<RegionDTO> createRegion(@RequestBody @Valid RegionDTO regionRequest) {
  var savedDTO = regionService.createRegion(regionRequest);
  var resourceLocation = URI.create("/api/regions" + savedDTO.getId());
  return ResponseEntity.created(resourceLocation).build();
}

After these changes, my applications prevent the storage of empty data, provide support for storing unique data when needed and, in addition, document the whole thing in a friendly way so that my application’s customers know what data model to expect.

Summarizing

Whenever you are working on creating a model for your application or simply a POST endpoint that retrieves data and stores it in the database, stop for a moment and consider which fields and relationships are essential. Consider which data should be unique and which should not be empty. Remember that in most cases, when you create a foreign key, it should also be marked as not null. Great if you provide tests that cover these cases.

If you happen to harbor any uncertainties or queries, do not hesitate to consult with your analyst, architect, or even seek guidance from another experienced developer. Remember, data holds paramount importance in the application you are currently developing, in most cases acting as the backbone of its functionality

Możesz również polubić…

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *