Design the APIs: Create a Category
Learn how to design the APIs for creating a new category.
Once the basic structure is ready, it’s time to add some products and categories for our e-commerce store.
For example, we can have a category of shoes and have different types of shoes as products. So, one category can have many products, but each product will belong to one category.
Model
Spring Boot JPA is a Java specification for managing relational data in Java applications. It allows us to access and persist data between Java object/ class and relational database.
First, we’ll create a model, Category. Go ahead and create a directory called model. Inside model
, we’ll create a file labeled Category.java. It will have the following four fields:
id
categoryName
description
imageUrl
We’ll also create setters, getters, and constructors for the four fields.
It will have an @Entity
annotation, indicating it is a JPA entity. It also has a @Table
annotation, which means this table will be mapped to a categories
table in the database.
package com.educative.ecommerce.model;import javax.persistence.CascadeType;import javax.persistence.Column;import javax.persistence.Entity;import javax.persistence.FetchType;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.OneToMany;import javax.persistence.Table;import javax.validation.constraints.NotBlank;import java.util.Set;@Entity@Table(name = "categories")public class Category {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Integer id;@Column(name = "category_name")private @NotBlank String categoryName;private @NotBlank String description;private @NotBlank String imageUrl;public Category() {}public Category(@NotBlank String categoryName, @NotBlank String description) {this.categoryName = categoryName;this.description = description;}public Category(@NotBlank String categoryName, @NotBlank String description, @NotBlank String imageUrl) {this.categoryName = categoryName;this.description = description;this.imageUrl = imageUrl;}public String getCategoryName() {return this.categoryName;}public void setCategoryName(String categoryName) {this.categoryName = categoryName;}public String getDescription() {return description;}public void setDescription(String description) {this.description = description;}@Overridepublic String toString() {return "User {category id=" + id + ", category name='" + categoryName + "', description='" + description + "'}";}public String getImageUrl() {return imageUrl;}public void setImageUrl(String imageUrl) {this.imageUrl = imageUrl;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}}
We’ll use @NotBlank
annotation for the category. For that, we have to include the following dependency in the pom.xml
file.
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId></dependency>
Repository
Here, we’ll create a repository, CategoryRepository.java
, as an interface that will extend the JpaRepository
interface. When CategoryRepository
is autowired to a service, the resulting object will be capable of directly communicating with the database using the methods in JpaRepository
.
package com.educative.ecommerce.repository;import com.educative.ecommerce.model.Category;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.stereotype.Repository;@Repositorypublic interface CategoryRepository extends JpaRepository<Category, Integer> {}
Service
Now we will create a service
package We’ll create a service package and inside it, we’ll create a CategoryService.java
file. The service will autowire CategoryRepository
. This class has the @Service
annotation.
Note: The repository talks to the model, the service talks to the repository, and the controller talks to the service.
The CategoryRepository
repository has inbuilt methods, findAll()
and save()
, as it is extending JpaRepository
.
package com.educative.ecommerce.service;import com.educative.ecommerce.model.Category;import com.educative.ecommerce.repository.Categoryrepository;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import javax.transaction.Transactional;import java.util.List;import java.util.Optional;@Servicepublic class CategoryService {@Autowiredprivate CategoryRepository categoryRepository;}
Controller
We’ll create a controller that will contain all the APIs. Create a directory/package controllers
, and inside the directory, create a file CategoryController.java
. We’ll autowire CategoryService
here.
We’ll use CategoryService
and CategoryRepository
, to interact with the database.
package com.educative.ecommerce.controllers;import com.educative.ecommerce.service.CategoryService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/category")public class CategoryController {@Autowiredprivate CategoryService categoryService;}
Setup code
/* * Copyright 2007-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } }
We’ll create a helper class called ApiResponse.java
inside a config
package, which will be used to return a response for the APIs.
package com.educative.ecommerce.common;import java.time.LocalDateTime;public class ApiResponse {private final boolean success;private final String message;public ApiResponse(boolean success, String message) {this.success = success;this.message = message;}public boolean isSuccess() {return success;}public String getMessage() {return message;}public String getTimestamp() {return LocalDateTime.now().toString();}}
We will create three APIs for category:
- Create
- Update
- List all categories
Create a new category
We’ll create an API endpoint in CategoryController
.
First, we’ll check if a category with a similar name exists. If not, we’ll create a new category by calling the createCategory(category)
method inside categoryService
.
@PostMapping("/create")public ResponseEntity<ApiResponse> createCategory(@Valid @RequestBody Category category) {if (Objects.nonNull(categoryService.readCategory(category.getCategoryName()))) {return new ResponseEntity<ApiResponse>(new ApiResponse(false, "category already exists"), HttpStatus.CONFLICT);}categoryService.createCategory(category);return new ResponseEntity<>(new ApiResponse(true, "created the category"), HttpStatus.CREATED);}
Let’s create the readCategory
method in service.
public Category readCategory(String categoryName) {return categoryRepository.findByCategoryName(categoryName);}
It will call the findByCategoryName()
in categoryRepository
, which we’ll create next.
public interface CategoryRepository extends JpaRepository<Category, Integer> {Category findByCategoryName(String categoryName);}
If a category exists by name, it will return it, or return null. The magic of JpaRepository
is it’s automated functionality.
Alternatively, we can make this field unique in the database.
Next, we’ll create the createCategory
method in service.
public void createCategory(Category category) {categoryRepository.save(category);}
Add Swagger dependency
We’ll also add swagger
for easy testing of the code. Create a file, SwaggerConfig
, in config
package. Remember to put the correct basePackage
in line 22.
package com.educative.ecommerce.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import springfox.documentation.builders.ApiInfoBuilder;import springfox.documentation.builders.PathSelectors;import springfox.documentation.builders.RequestHandlerSelectors;import springfox.documentation.service.ApiInfo;import springfox.documentation.service.Contact;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.Docket;import springfox.documentation.swagger2.annotations.EnableSwagger2;@Configuration@EnableSwagger2public class SwaggerConfig {@Beanpublic Docket productApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(getApiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.educative.ecommerce")).paths(PathSelectors.any()).build();}private ApiInfo getApiInfo() {Contact contact = new Contact("webtutsplus", "http://webtutsplus.com", "contact.webtutsplus@gmail.com");return new ApiInfoBuilder().title("Ecommerce API").description("Documentation Ecommerce api").version("1.0.0").license("Apache 2.0").licenseUrl("http://www.apache.org/licenses/LICENSE-2.0").contact(contact).build();}}
Add H2 dependency
We need to also add these dependencies in the pom.xml
file for swagger
and the h2 in-memory database.
Note: You can choose any database of your choice.
<dependency><groupId>io.springfox</groupId><artifactId>springfox-bean-validators</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
Change application.properties
We also have to modify our application.properties
file by adding the below lines of code:
spring.datasource.url=jdbc:h2:mem:testdbspring.datasource.driverClassName=org.h2.Driverspring.datasource.username=saspring.datasource.password=passwordspring.jpa.database-platform=org.hibernate.dialect.H2Dialect
The final code
/* * Copyright 2007-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } }