Name | Version | Required | MacOS Guide | Notes |
latest | true | The installation package typically installs both Git and Git Bash. | ||
17.0.4 | true | If you are using an older version of openjdk (minimum v11+), you can still run this project by either setting VM options in the Run Config or appending the following to the bash command below: | ||
3.8.6 | true | |||
6.0 | false | Use an embedded version of MongoDB. More info under the database related sections. |
Type | OS | Options |
Bash Emulator | MacOS | Native Terminal, iTerm2 |
Bash Emulator | Windows | |
IDE | MacOS, Windows |
For this codelab, we will design and develop five RESTful API endpoints. The API will have a service class that calls an external stock API to populate a MongoDB, which the five endpoints will interact with. The API contract will contain the following resource methods:
Endpoint | Implementation |
Get all stock objects | GET |
Get a single stock object | GET |
Create a new stock object | POST |
Update a stock object | PUT |
Delete a stock object | DELETE |
Ideal software development occurs in two distinct phases:
Spec Driven Development is the process of generating a concise spec that can be used to describe your application's interactions in a pragmatic way. In other words, the Spec is a blueprint for your application, detailing how the user interacts with it, rather than just expected behaviors/results. In order to be successful with Spec Driven Development, the Spec must be:
We will be utilizing Swagger framework to design, produce, visualize, and consume our RESTful service. It provides a programming language-agnostic interface, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code.
✅ Move on to the next step to start building your API Spec!
An OpenAPI spec can be written in either JSON
or YAML
. We will be using YAML
for this code lab.
Use the online swagger editor 👉🏼 editor.swagger.io
In this codelab, we will create a simple API that stores data in MongoDB.
Let's begin with the basic info
block at the start of the file.
openapi: 3.0.3
info:
title: Stock API
description: Sample Java Spring Boot API using MongoDB Atlas
contact:
name: Zarin Lokhandwala
url: https://github.com/zarinlo
version: 1.0.0
servers:
- url: http://localhost:8080
description: Inferred Url
tags:
- name: stock-controller
description: Stock Controller
Now let's break it down:
Metadata | Details |
| The version of the OpenAPI spec you are using (i.e. 3.0.3) |
| The info block contains important meta-details regarding your API |
| The version of the API being developed, which should follow Semantic Versioning. Semantic versioning consists of three digits, the first numnber indicating the major version of an application, the next number is known as minor which indicates any features that have been added, and the last number indicates the patch fix that has been applied. The major number is what is actually taken into consideration when determining the base path of an API. |
| The URL that is hosting/serving the API. More than one entry can be made under the |
| Tags are used to group or categorize operations together for a specific reason. |
Let's add the first REST endpoint definition: GET /stocks
endpoint.
Before we do so, we must define the schemas for the request and response objects from the Latest Stock API (reference the Overview slide for more details) inventory.
Insert the following section directly after the tags
section.
components:
schemas:
Stock:
title: Stock
type: object
properties:
dayHigh:
type: number
format: double
dayLow:
type: number
format: double
identifier:
type: string
lastPrice:
type: number
format: double
lastUpdateTime:
type: string
open:
type: number
format: double
previousClose:
type: number
format: double
symbol:
type: string
totalTradedValue:
type: number
format: double
totalTradedVolume:
type: integer
format: int64
yearHigh:
type: integer
format: int64
yearLow:
type: integer
format: int64
In the code block above, we are defining the Stock object that has a number of attributes.
Under the properties
tag, each attribute is given a type
and a format
. You can elaborate on the types and formats, just do a quick google search.
Now let's take a step back. APIs, typically, produce and consume the Content-Type
known as JSON. The /stocks
endpoint should, therefore, return a JSON response object. Furthermore, in this case, an array of stock objects.
The correct way to return to return all the stock objects, according to REST, is to return an object encompassing an array of objects:
{
"stocks": [
{},
{},
{}
]
}
The incorrect way to return all the stock objects is to return a top-level array:
[
{},
{},
{}
]
An important rule in REST is to design a consistent API spec. To return either a stock object or a collection of stock objects, we define a general response object.
The response object will encompass any response data type and deliver it back to the user in a standard format.
When defining the general object, do so under the schemas
section of the YAML and ensure the indentation is precise, since YAML files are senstiive.
components:
schemas:
...
StockGeneralResponse:
title: StockGeneralResponse
type: object
properties:
response:
type: object
status:
type: string
enum:
- ACCEPTED
- BAD_GATEWAY
- BAD_REQUEST
- CREATED
- NOT_FOUND
- NO_CONTENT
- OK
userMessages:
uniqueItems: true
type: array
items:
type: string
This response object has three main attributes:
Metadata | Details |
| This is the actual response encompassed by the general object. Ergo a stock object or a collection of stock objects. |
| The HTTP status that was returned by the underlying service responsible for the data. An |
| This is used to deliver a string of messages back to the user to provide additional information incase an error occurs. |
✅ Move on to the next step to add the API endpoints.
Now we are ready to implement the GET /stocks
method. We are going to insert this portion between the basic info
block we created and the components
block.
/stocks
paths:
"/api/v1/stocks":
get:
tags:
- stock-controller
summary: Get all stocks
operationId: getAllStocks
responses:
'200':
description: 'Successful: Stock(s) found.'
content:
application/json:
schema:
"$ref": "#/components/schemas/StockGeneralResponse"
'400':
description: 'Bad Request: Check input parameter(s) syntax for invalid characters.'
'401':
description: 'Unauthorized: User is not entitled to retrieve information.'
'404':
description: 'Not Found: Stock(s) not found.'
'500':
description: 'Internal Server Error: Backend service is down.'
Once again, here's the breakdown of the keys:
Metadata | Details |
| This is where you specify the base + context path, essentially the full endpoint. The base path is |
| The CRUD operation being applied to a given path. |
| Short description of the function. |
| The actual name of the function you are going to use in your source code, in this case, |
| A list of possible HTTP status codes returned by the server as a response to the client. Take a look at HTTP status codes to understand which HTTP numeric response is appropriate for a given scenario. |
| Content type to be returned to the user is in the form of JSON, denoted as |
| Sets the data-type of the content being returned by the function. A method will not always return content. For instance, |
Now, under the endpoint of /api/v1/stocks
, we are implementing a GET
operation and a POST
operation.
Here is how we are going to define a post
operation, which should be aligned under the get
defintion we completed above.
/stocks
paths:
"/api/v1/stocks":
...
post:
tags:
- stock-controller
summary: Create a new stock
operationId: createStock
requestBody:
content:
application/json:
schema:
"$ref": "#/components/schemas/Stock"
responses:
'201':
description: 'Created: Stock created.'
content:
application/json:
schema:
"$ref": "#/components/schemas/StockGeneralResponse"
'400':
description: 'Bad Request: Check input parameter(s) syntax for invalid characters.'
'401':
description: 'Unauthorized: User is not entitled to retrieve information.'
'404':
description: 'Not Found: Stock(s) not found.'
'500':
description: 'Internal Server Error: Backend service is down.'
When creating an object via a POST
operation, the client passes the server some data.
Some forms of how a client can pass data to the server are:
Type | Example |
a query parameter | Any value assigned to a key after the |
a path parameter |
|
an object sent via the body of a request |
|
a Header value |
|
a browser cookie 🍪 | Logins, shopping carts, game scores, or anything else the server should remember |
In this case, the POST
will be performed with a stock object via the body of the request.
Therefore, the requestBody
is the main difference between the attributes used to describe the POST
operation as compared to the GET
.
👉🏽 To learn more about parameter types, check out the Describing Parameters page.
This concludes the spec for the CRUD operations that can be performed under the /api/v1/stocks
endpoint.
✅ Move on to the next step to complete your API spec.
Let's implement the remaining three endpoints that follow the path of: /api/v1/stocks/{symbol}
/stocks/{symbol}
paths:
...
"/api/v1/stocks/{symbol}":
get:
tags:
- stock-controller
summary: Get a stock by symbol
operationId: getStockBySymbol
parameters:
- name: symbol
in: path
description: A stock symbol
required: true
style: simple
allowReserved: false
schema:
type: string
responses:
'200':
description: 'Successful: Stock(s) found.'
content:
application/json:
schema:
"$ref": "#/components/schemas/StockGeneralResponse"
'400':
description: 'Bad Request: Check input parameter(s) syntax for invalid characters.'
'401':
description: 'Unauthorized: User is not entitled to retrieve information.'
'404':
description: 'Not Found: Stock(s) not found.'
'500':
description: 'Internal Server Error: Backend service is down.'
put:
tags:
- stock-controller
summary: Update an existing stock by symbol
operationId: updateStockBySymbol
parameters:
- name: symbol
in: path
description: A stock symbol
required: true
style: simple
schema:
type: string
- name: lastPrice
in: query
description: Last Price
required: true
style: form
schema:
type: number
format: double
responses:
'202':
description: 'Accepted: Stock updated.'
content:
application/json:
schema:
"$ref": "#/components/schemas/StockGeneralResponse"
'400':
description: 'Bad Request: Check input parameter(s) syntax for invalid characters.'
'401':
description: 'Unauthorized: User is not entitled to retrieve information.'
'404':
description: 'Not Found: Stock(s) not found.'
'500':
description: 'Internal Server Error: Backend service is down.'
delete:
tags:
- stock-controller
summary: Delete a stock by symbol
operationId: deleteStockBySymbol
parameters:
- name: symbol
in: path
description: A stock symbol
required: true
style: simple
schema:
type: string
responses:
'204':
description: 'No Content: Stock deleted.'
content:
application/json:
schema:
"$ref": "#/components/schemas/StockGeneralResponse"
'400':
description: 'Bad Request: Check input parameter(s) syntax for invalid characters.'
'401':
description: 'Unauthorized: User is not entitled to retrieve information.'
'404':
description: 'Not Found: Stock(s) not found.'
'500':
description: 'Internal Server Error: Backend service is down.'
Take a look at how the paths are defined for /api/v1/stocks
and /api/v1/stocks/{symbol}
. You will notice that they are surrounded by quotes. In YAML, enclosing characters in quotes ensures that it will be handled as a string
.
Each of the operations above will require a path
parameter denoted as:
in: path
For the rest of the items, refer to the link presented earlier: Describing Parameters
🚀 You've completed your first basic API spec! Now let's start by scaffolding (i.e. structuring) your Spring Boot project.
A tool that scaffolds your web project for you, is a tool that helps you kickstart new projects, which presecribes best practices and folder structure to help you stay productive. We will use Spring Initialzr to scaffold your basic Spring boot API.
Spring Initialzr can be accessed via a web UI or through your IDE (i.e. IntelliJ/Eclipse). It generates a minimal project with the dependencies of your choice and enables you to start developing quickly.
Type | Example |
Group |
|
Artifact |
|
Name | Stocks API |
Description | Sample Java Spring Boot API using MongoDB Atlas |
Package name |
|
Packaging | Jar |
Before we begin developing our API, let's setup the structure of the project correctly. Once you extract the initial zip file from the previous step, ensure your directory structure looks like the following:
|api
|-src
| |-main
| | |-java
| | | |-sample
| | | | |-api
| | |-resources
| | | |-static
| | | |-templates
| |-test
You should rename the initial api folder to something else, for instance, sample-springboot-api.
|sample-springboot-api
|-src
| |-main
| | |-java
| | | |-sample
| | | | |-api
| | |-resources
| | | |-static
| | | |-templates
| |-test
Now, let's create the other directories that we will need going further.
|sample-springboot-api
|-src
| |-main
| | |-java
| | | |-sample
| | | | |-api
| | | | | |-stocks
| | | | | | |-repositories
| | | | | | |-models
| | | | | | |-exceptions
| | | | | | |-configs
| | | | | | |-controllers
| | | | | | |-services
| | |-resources
| | | |-static
| | | |-templates
| |-test
Let's breakdown the folders under stocks:
Component | Details |
controllers | Manages all the REST calls and status codes |
services | The business logic layer that handles any manipulation of data required |
repositories | ses a Java Persistence API (JPA) that analyzes all the methods defined by an interface and automatically generates queries from the method names, in order to simplify the connection to the database from the Service layer |
configs | Sets up the configurations for the REST calls, web security, Swagger documentation, etc |
models | Manages all the REST calls and status codes |
exceptions | Develop custom error handling for the application |
Apache Maven, referred to as maven, is a build management tool that is primarily used to build Java projects.
To learn more, check out: Understanding Apache Maven - The Series
Now before we import your project into an IDE, specifically IntelliJ for this tutorial, let's configure your maven settings.
Verify that maven is installed correctly by running the following:
05:00PM ~/.m2
🦋 mvn --version
Apache Maven 3.5.3 (3383c37e1f9e9b3bc3df5050c29c8aff9f295297; 2018-02-24T14:49:05-05:00)
Maven home: /opt/apache-maven-3.5.3
Java version: 16.0.1, vendor: Homebrew
Next, you will need to inlcude a settings.xml
file directly under your .m2 folder. Your .m2 folder should located under your user home directory, as shown in the snippet above.
The settings.xml
file does not need anything in specific for the time being, the stock file is good enough.
This file has metadata that is used by maven to install dependencies, understand which mirrors (i.e public repositories) to download said dependencies from, set proxies to circumvent firewalls, etc.
More information on a default settings.xml
file here: Apache Maven Settings
If you are using IntelliJ, import your project as a Maven project.
Run through the following to make sure your project has been imported and configured correctly.
Import the project as a Maven project. Continue through the wizard and let all the dependencies load, which may take some time.
Under Preferences OR File –> Settings, go to Build, Execution. Deployment –> Build Tools –> Maven, and make sure the remote repository URLs are being pulled from your settings.xml
file.
Under File –> Project Structure –> Project, make sure that you set your project SDK to version of Java you are using.
Under File –> Project Structure –> Modules, make sure that you set your modules to match the version of Java you are using.
In order to run your project, go ahead and setup a Spring Boot run configuration.
Once you have imported the project, there may be times where you need to reimport dependencies incase you add/change/remove dependencies. Try the following:
Ctrl
+Shift
+A
to find actions, and input "reimport", you will find the option. On a Mac, use ⌘ + ⇧ + A instead.✅ Continue on to the next step to start creating the Object model classes for a Stock as we defined in our API Spec.
Throughout this tutorial, the expectation is that the user will be commiting code often to one of the following source code repositories: GitHub, GitLab (use what is available to you)
That being said, there are many files produced by your OS, or IDE, or a build tool, that are not required to be versioned inside a code repo. Therefore, we ignore those files when pushing to a source code repo.
Fortunately, there is a website that will help generate this file, called .gitignore
. The file is a plain text file where each line contains a pattern for files/directories to ignore. Generally, this is placed in the root folder of the repository, and that's what I recommend. However, you can put it in any folder in the repository and you can also have multiple.
Further, if you forget to add a specific file to the .gitignore
, there is a way to untrack that file before pushing the code.
You can learn more here: Gitignore Explained: What is Gitignore and How to Add it to Your Repo
Go here: gitignore.io
Enter the values for the OS, IDE, build tools, etc, that are relevant to your development setup:
Press "Create" and this should open up a browser window with the detailed inputs for your .gitignore
file.
Here is the sample output:
.gitignore
# Created by https://www.toptal.com/developers/gitignore/api/intellij+iml,maven
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+iml,maven
### Intellij+iml ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
...
### Intellij+iml Patch ###
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
### Maven ###
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
# End of https://www.toptal.com/developers/gitignore/api/intellij+iml,maven
Copy the whole thing and create a .gitignore
file in the root directory of this project. Paste the values in the file and hit save.
✅ All done! Now let's get coding 😎
Create a class called Stock.java
under the models package (i.e. directory) with the following attributes:
double
dayHighdouble
dayLowString
identifierdouble
lastPriceString
lastUpdateTimedouble
opendouble
previousCloseString
symboldouble
totalTradedValuelong
totalTradedVolumelong
yearHighlong
yearLowYou will need to Generate getters and setters for this Object class so that you can access and modify the objects as needed. Under Code –> Generate select Getter and Setter and select all the attributes to generate them for. Repeat the same steps to generate a Constructor as well.
Stock.java
public class Stock {
public double dayHigh;
public double dayLow;
public String identifier;
public double lastPrice;
public String lastUpdateTime;
public double open;
public double previousClose;
public String symbol;
public double totalTradedValue;
public long totalTradedVolume;
public long yearHigh;
public long yearLow;
// generate empty constructor
public Stock() {
}
public double getDayHigh() {
return dayHigh;
}
public void setDayHigh(double dayHigh) {
this.dayHigh = dayHigh;
}
public double getDayLow() {
return dayLow;
}
public void setDayLow(double dayLow) {
this.dayLow = dayLow;
}
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public double getLastPrice() {
return lastPrice;
}
public void setLastPrice(double lastPrice) {
this.lastPrice = lastPrice;
}
public String getLastUpdateTime() {
return lastUpdateTime;
}
public void setLastUpdateTime(String lastUpdateTime) {
this.lastUpdateTime = lastUpdateTime;
}
public double getOpen() {
return open;
}
public void setOpen(double open) {
this.open = open;
}
public double getPreviousClose() {
return previousClose;
}
public void setPreviousClose(double previousClose) {
this.previousClose = previousClose;
}
public String getSymbol() {
return symbol;
}
public void setSymbol(String symbol) {
this.symbol = symbol;
}
public double getTotalTradedValue() {
return totalTradedValue;
}
public void setTotalTradedValue(double totalTradedValue) {
this.totalTradedValue = totalTradedValue;
}
public double getTotalTradedVolume() {
return totalTradedVolume;
}
public void setTotalTradedVolume(long totalTradedVolume) {
this.totalTradedVolume = totalTradedVolume;
}
public long getYearHigh() {
return yearHigh;
}
public void setYearHigh(long yearHigh) {
this.yearHigh = yearHigh;
}
public long getYearLow() {
return yearLow;
}
public void setYearLow(long yearLow) {
this.yearLow = yearLow;
}
}
Next, we need a class called StocksList.java
to return an array of stock objects.
StocksList.java
import java.util.List;
public class StockList {
List<Stock> stockList;
public StockList(List<Stock> stockList) {
this.stockList = stockList;
}
public List<Stock> getStockList() {
return stockList;
}
public void setStockList(List<Stock> stockList) {
this.stockList = stockList;
}
}
Now according to our API spec, we do NOT want to return a top-level array for the GET /api/v1/stocks
endpoint.
Therefore, we develop a general class called StocksGeneralResponse.java
.
StockGeneralResponse.java
import org.eclipse.collections.impl.set.mutable.UnifiedSet;
import org.springframework.http.HttpStatus;
public class StockGeneralResponse<T> {
private T response;
private HttpStatus httpStatus;
private UnifiedSet<String> userMessages = UnifiedSet.newSet();
public StockGeneralResponse() {
}
public StockGeneralResponse(T response, HttpStatus httpStatus) {
this.response = response;
this.httpStatus = httpStatus;
}
public T getResponse() {
return response;
}
public void setResponse(T response) {
this.response = response;
}
public HttpStatus getHttpStatus() {
return httpStatus;
}
public void setHttpStatus(HttpStatus httpStatus) {
this.httpStatus = httpStatus;
}
public UnifiedSet<String> getUserMessages() {
return userMessages;
}
public void setUserMessages(UnifiedSet<String> userMessages) {
this.userMessages = userMessages;
}
}
In the code snippet above, you will see the use of T
which stands for Template in this case.
The template allows the StockGeneralResponse
to encompass any data type delivered by the response into an object, and pass along the HTTP status for that response as well.
Certain datatypes used in this java class require the addition of Eclipse Collections. Therefore, open up your root pom.xml
and include:
<dependencies>
...
<!--Eclipse Collections-->
<dependency>
<groupId>org.eclipse.collections</groupId>
<artifactId>eclipse-collections-api</artifactId>
<version>${eclipse.collections}</version>
</dependency>
<dependency>
<groupId>org.eclipse.collections</groupId>
<artifactId>eclipse-collections</artifactId>
<version>${eclipse.collections}</version>
</dependency>
...
</dependencies>
Create or use the pre-existing < properties >
section at the top of the pom.xml
file:
<properties>
...
<eclipse.collections>10.2.0</eclipse.collections>
</properties>
✅ Now let's get started on designing our first API endpoint!
Annotations in Java is a special form of metadata that can be embedded in Java source code. Users can use annotations to configure beans inside the java source file itself.
Here is a full set of all available annotations within the Spring Framework: Spring Framework Annotations
This annotation is used on classes to indicate a Spring component. The @Component
annotation marks the Java class as a bean or say component so that the component-scanning mechanism of Spring can add into the application context.
The @Controller
annotation is used to indicate the class is a Spring controller. This annotation can be used to identify controllers for Spring MVC or Spring WebFlux.
This annotation is used on a class. The @Service
marks a Java class that performs some service, such as execute business logic, perform calculations and call external APIs. This annotation is a specialized form of the @Component
annotation intended to be used in the service layer.
This annotation is used on Java classes which directly access the database. The @Repository
annotation works as marker for any class that fulfills the role of repository or Data Access Object. This annotation has a automatic translation feature. For example, when an exception occurs in the @Repository
there is a handler for that exception and there is no need to add a try catch block.
This annotation is used on classes which define beans. @Configuration
is an analog for XML configuration file – it is configuration using Java class. Java class annotated with @Configuration
is a configuration by itself and will have methods to instantiate and configure the dependencies.
This annotation is used at the method level. @Bean
annotation works with @Configuration
to create Spring beans. As mentioned earlier, @Configuration
will have methods to instantiate and configure dependencies. Such methods will be annotated with @Bean
. The method annotated with this annotation works as bean ID and it creates and returns the actual bean.
This annotation is used at the field, constructor parameter, and method parameter level. The @Value
annotation indicates a default value expression for the field or parameter to initialize the property with.
This annotation is used on the application class while setting up a Spring Boot project. The class that is annotated with the @SpringBootApplication
must be kept in the base package.
This annotation is used at the class level. The @RestController
annotation marks the class as a controller where every method returns a domain object instead of a view.
This annotation is a method level annotation. The @Scheduled
annotation is used on methods along with the trigger metadata. A method with @Scheduled
should have void return type and should not accept any parameters.
In Java there is a concept of interfaces and classes. An interface specifies what a class must do, and not how.
The first interface we are going to define is StockService.java
and it will be under the services folder. Here we declare the behaviors of all the functions for our five main endpoints, which the class will implement.
Let's review the endpoints:
Usage | Function | Implementation |
Get all stock objects |
| GET |
Get a single stock object |
| GET |
Create a new stock object |
| POST |
Update [the price of] a stock object |
| PUT |
Delete a stock object |
| DELETE |
This is what our interface looks like:
StockService.java
import sample.api.stocks.exceptions.StockResponseException;
import sample.api.stocks.models.Stock;
import sample.api.stocks.models.StockGeneralResponse;
public interface StockService {
void populateStockDatabase();
StockGeneralResponse getAllStocks();
StockGeneralResponse getStockBySymbol(String symbol);
StockGeneralResponse createStock(Stock stock);
StockGeneralResponse updateStock(String symbol, Double lastPrice);
StockGeneralResponse deleteStock(String symbol);
}
populateStockDatabase()
function that will help us populate our database and keep it updated at a given frequency.StockGeneralResponse
for each endpoint, even for the DELETE
operation, which typically returns no content. More on this later.First, let's populate the data so that we have some data to manipulate.
We are going to work on writing the populateStockDatabase()
. This function will be defined in a new class called, StockServiceImpl.java
, which is the implementation of the interface we just created.
Before we get started, we are going to have to do the following:
repository
that will handle the data stored in the MongoDB instanceTo make things easier, I have created a subscription and have retrieved a sample key from the Latest Stock API.
Here are some points you will need to know about consuming the API:
Key | Value | Notes |
Invocation URL |
| This is the base URL that will be configured in the REST template config to consume stock data. |
API Key |
| In the HTTP request, |
Path |
| The context path to view the prices for a specific stock index. |
Query Parameter |
| We will use the stock index, |
To make a test call to this API, we are going to use Postman.
Postman is an API client that makes it easy for developers to create and save simple and complex HTTP/s requests, as well as read their responses. The result - more efficient and less tedious work.
In Postman, setup a simple GET
request with the following values to target:
https://latest-stock-price.p.rapidapi.com/price?Indices=NIFTY NEXT 50
Note: NIFTY NEXT 50
will be HTML URL encoded by Postman, and that is not something the user has to be concerned with.
Hit the Send button and your response should look as followed:
[
{
"symbol": "NIFTY NEXT 50",
"identifier": "NIFTY NEXT 50",
"open": 38759.15,
"dayHigh": 38846.85,
"dayLow": 38684.95,
"lastPrice": 38697.7,
"previousClose": 38678.15,
"change": 19.549999999995634,
"pChange": 0.05,
"yearHigh": 39399.6,
"yearLow": 25614.15,
"totalTradedVolume": 28602611,
"totalTradedValue": 7654580126.95,
"lastUpdateTime": "30-Jun-2021 09:29:14",
"perChange365d": 50.15,
"perChange30d": 3.43
},
{ ... }
]
In order to interact with MongoDB, we will be using the Spring Framework MongoDB data repository. Spring data repositories simplify the amount of code required to implement data access layers for various persistence stores.
All that's required to get this working is to create an interface that extends the Spring MongoDB repository. So let's create a StockRepository.java
interface that extends
the MongoRepository
, under the repositories package/directory. We want to annotate this persistence layer with @Repository
, which again, is a stereotype annotation applied to data access layers that are used to get data from the database.
Out-of-the-box, this interface comes with many operations, including standard CRUD (create-read-update-delete) operations. Spring has its' own query builder mechanism and the findAll
method is already provided by default, so for now, we will leave this interface empty of additional queries.
When you extend the interface, the < Stock, String >
implies that the MongoDB will contain stock objects, stored as strings.
StockRepository.java
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import sample.api.stocks.models.Stock;
@Repository
public interface StockRepository extends MongoRepository<Stock, String> {
}
Let's take a quick step back and go back to the Stock.java
object model class. You are going to annotate the symbol attribute with @Id
. This tells the MongoDB that we want map the _id
field in mongo to the stock symbol, which is essentially the unique identifier for each object in the databse.
You are also going to annotate the whole class with @Document
to define the name of the document collection where the data should be stored within the MongoDB database.
Stock.java
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "stocks")
public class Stock {
...
@Id
public String symbol;
public double totalTradedValue;
public long totalTradedVolume;
public long yearHigh;
public long yearLow;
...
}
In order to connect to the Latest Stock API we will create an application properties file to manage the configuration items. The application.yaml
file will be created under the resources directory.
application.yaml
# stocks api
stocks:
api:
fqdn: https://latest-stock-price.p.rapidapi.com
fqdn
stands for fully qualified domain name 🤓Now, under the configs folder, create a class called RestTemplateConfig.java
. This template will help establish a connection to the Latest Stock API at application runtime.
RestTemplateConfig.java
package sample.api.stocks.configs;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;
@Configuration
public class RestTemplateConfig {
@Value("${stocks.api.fqdn}")
private String stocksApiUrl;
@Bean
@Qualifier("stocksApiRestTemplate")
public RestTemplate stocksApiRestTemplate() throws Exception {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(this.stocksApiUrl));
return restTemplate;
}
}
Let's go over some things in this configuration class:
@Configuration
, which is a configuration by itself and will have methods to instantiate and configure the dependencies.@Value
tells the project to initialize the string
property stocksApiUrl
with an item found in the application.yaml
file that follows the hierarchy of: stocks > api > fqdn. If this variable is not found, the application will not start up.stocksApiUrl
to the template handler to establish a connection with the stock API.When an error occurs within any of our methods, the method will create an object and hand it off to the runtime system. The object, called an exception object, contains information about the error, including its type and the state of the program when the error occurred. Creating an exception object and handing it to the runtime system is called throwing an exception. More here: Oracle: Java Tutorials - Exceptions
Now when an appropriate handler is found, the runtime system passes the exception to the handler. An exception handler is considered appropriate if the type of the exception object thrown matches the type that can be handled by the handler. The exception handler chosen is said to catch the exception.
Let's start by creating a simple exception class under the exceptions folder.
StockResponseException.java
public class StockResponseException extends Exception {
public StockResponseException(String message) {
super(message);
}
}
This is a pretty standard exception class defined in java.
We will take it a step further by creating an exception handler as well. More on this later.
At this point we have setup all the items needed to make this rest call to the external API. Let's break down how we are going to do this by creating the following class under the services folder.
StockServiceImpl.java
import org.eclipse.collections.impl.list.mutable.FastList;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import sample.api.stocks.exceptions.StockResponseException;
import sample.api.stocks.models.*;
import sample.api.stocks.repositories.StockRepository;
import java.util.Objects;
// annotation which marks a Java class that executes business logic, perform calculations and calls external APIs
@Service
public class StockServiceImpl implements StockService {
// define local variable to call the stock repository (i.e. MongoDB)
private final StockRepository stockRepository;
// define local var that will perform the connection to the external API
private final RestTemplate stocksApiRestTemplate;
// define local var to pass the API key via headers
private final HttpHeaders httpHeaders;
public StockServiceImpl(
// the @Qualifier annotation will ensure that the correct REST template is being instantiated in the constructor
@Qualifier("stocksApiRestTemplate") RestTemplate stocksApiRestTemplate,
StockRepository stockRepository) {
this.stocksApiRestTemplate = stocksApiRestTemplate;
this.stockRepository = stockRepository;
// instantiating headers and passing in the API key to the header name "x-rapidapi-key", which can be found in the API documentation
httpHeaders = new HttpHeaders();
httpHeaders.add("x-rapidapi-key", "9e87a2c143msh6b92309e36af212p15ccc6jsn2bc37ea481bd");
}
//---------------------------------------------------------------------------------------------------------------
// annotation is used to trigger this request once a minute, time in seconds
@Scheduled(fixedRate = 60000)
public void populateStockDatabase() throws StockResponseException {
// the path that needs to be called once the rest template intializes a connection with the API
String path = "/price";
// a query parameter was required to be passed to the path above
// and we utilize a query builder object where "Indices=NIFTY%20NEXT%2050" is the HTML URL encoded value
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(path)
.queryParam("Indices", "NIFTY NEXT 50");
// a FastList is an attempt to provide the same functionality as ArrayList without the support for concurrent modification exceptions
// we set the type of data that will be collected in this FastList, i.e. Stock
// we use the rest template and apply the "exchange" function to it
// the exchange method takes in the builder object which will result in --> /price?Indices=NIFTY%20NEXT%2050
// we pass in the CRUD operation as GET and pass in the headers that were previously instantiated
ResponseEntity<FastList<Stock>> response = stocksApiRestTemplate.exchange(builder.build().toString(), HttpMethod.GET, new HttpEntity<>(httpHeaders), new ParameterizedTypeReference<FastList<Stock>>() {
});
// if the response status is a 200 or OK...
if (response.getStatusCode() == HttpStatus.OK) {
// we ensure that the response is not null, and then save the body of the response object into our database via the stock respository's default given method, save()
Objects.requireNonNull(response.getBody()).forEach(stockRepository::save);
} else {
// if an issue occurs, we throw an exception with a specific error message, in String format
throw new StockResponseException("Error: Issue retrieving stocks.");
}
}
}
Since we decided to throw exceptions within our StockServiceImpl.java
class, therefore, we need to update our interface class to reflect the same.
StockService.java
import sample.api.stocks.exceptions.StockResponseException;
import sample.api.stocks.models.Stock;
import sample.api.stocks.models.StockGeneralResponse;
public interface StockService {
void populateStockDatabase() throws StockResponseException; // throws exception
StockGeneralResponse getAllStocks();
StockGeneralResponse getStockBySymbol(String symbol);
StockGeneralResponse createStock(Stock stock);
StockGeneralResponse deleteStock(String symbol);
StockGeneralResponse updateStock(String symbol, Double lastPrice) throws StockResponseException; // throws exception
}
Now, navigate to the main Spring Boot application class. In order to trigger the populateStockDatabase()
function at a scheduled rate, we must annotate the main class with @EnableScheduling
.
ApiApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class ApiApplication {
public static void main(String[] args) {
SpringApplication.run(ApiApplication.class, args);
}
}
Now that you have setup the service that is going to populate the database, let's build a service that will retrieve all the records from the database, i.e. getAllStocks()
.
In the service implementation class, define the following function.
StockServiceImpl.java
public StockGeneralResponse getAllStocks() {
return new StockGeneralResponse(stockRepository.findAll(), HttpStatus.OK);
}
All we are doing is calling the stock repository's findAll
method, which is one of the default methods provided by the JPA's as stated in the Setup Stock MongoDB Repository section.
To view the stock data, let's configure our stock controller class under the controllers directory.
The controller class is responsible for defining and exposing endpoints in our API contract. It is also responsible for delivering the correct HTTP status to the user.
We will utilize the OpenAPI, also known as Swagger, framework to autogenerate and manage the endpoints defined in our controller. The framework provides us access to various annotations that help frame the API documentation.
Open up your root pom.xml
and include these dependencies:
<dependencies>
...
<!--OpenAPI 3-->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-data-rest</artifactId>
<version>${springdoc.version}</version>
</dependency>
...
</dependencies>
Use the pre-existing < properties >
section at the top of the pom.xml
file and include:
<properties>
...
<springdoc.version>1.6.11</springdoc.version>
</properties>
Now let's setup the stock controller:
StockController.java
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import sample.api.stocks.models.StockGeneralResponse;
import sample.api.stocks.services.StockService;
// include the following media type constants
import static org.springframework.http.MediaType.ALL_VALUE;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
// annotatation denotes that this class will handle HTTP requests
@RestController
// request mapping sets the root API context of /api/v1 and every endpoint created in this controller will begin with that context
// instead of creating headers for Content-Type and Accept, we set these headers as consumes and produces within the annotation
@RequestMapping(
value="/api/v1",
consumes = {APPLICATION_JSON_VALUE, APPLICATION_FORM_URLENCODED_VALUE, ALL_VALUE},
produces = {APPLICATION_JSON_VALUE})
// we define the HTTP response codes and their messages to be applied to each endpoint defined in this controller
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Successful: Stock(s) found."),
@ApiResponse(responseCode = "201", description = "Created: Stock created."),
@ApiResponse(responseCode = "202", description = "Accepted: Stock updated."),
@ApiResponse(responseCode = "204", description = "No Content: Stock deleted."),
@ApiResponse(responseCode = "400", description = "Bad Request: Check input parameter(s) syntax for invalid characters."),
@ApiResponse(responseCode = "401", description = "Unauthorized: User is not entitled to retrieve information."),
@ApiResponse(responseCode = "404", description = "Not Found: Stock(s) not found."),
@ApiResponse(responseCode = "500", description = "Internal Server Error: Backend service is down.")
})
public class StockController {
// define local var that will allow us to call our stock service
// note: this is NOT The stock service implementation, we are calling the interface
private final StockService stockService;
public StockController(StockService stockService) {
this.stockService = stockService;
}
//---------------------------------------------------------------------------------------------------------------
// annotatation verbalizes what the endpoint is used for
@Operation(summary = "Get all stocks")
// use the annotation that is specific to the GET operation
@GetMapping(value = "/stocks")
// in the controller it is common to return the ResponseEntity as the response object type
public ResponseEntity<StockGeneralResponse> getAllStocks() {
ResponseEntity<StockGeneralResponse> responseEntity;
// call the getAllStocks method from the stock service
StockGeneralResponse serviceResponse = stockService.getAllStocks();
responseEntity = new ResponseEntity<>(serviceResponse, serviceResponse.getHttpStatus());
return responseEntity;
}
}
At this point, we have done everything required to call the external API, so now let's actual connect to the database and ensure .
Before we do that, let's learn about how to manage application properties correctly.
Spring allows you to configure/enable/disable various application properties rather easily via an application.yaml
or an application.properties
file. These files are found under the src/main/resources folder.
Spring also allows you to handle properties per environment (i.e. dev, qa, prod). This is taken care of in the naming convention of the file. In order to select which Spring profile to use at runtime, we generate the correct .yaml
file.
In order to keep the environment properties organized, we will create the following files under src/main/resources
. Notice the naming convention we use in the files.
Active Profile / Environment | File |
Default |
|
local |
|
dev |
|
qa |
|
prod |
|
Spring will initilize everything in the environment specific file first and they take precedent than anything defined in the default application.yaml
file. It contains properties common to all profiles/environments at run time.
Earlier we utilized the application.yaml
file to include the fqdn
of the stocks API. The standard file means that no matter what profile we are running, the property will be available to all profiles, unless it is overwritten in a environment specific application-${env}.yaml
file.
Now there are multiple ways to set the runtime environment or in other words, the Active Profile. This goes back to configuring your run/debug configurations. If you look closely, there is an option to set your Active Profile, and in that section, you can type in local
or dev
, etc. For the purposes of this tutorial, let's set the profile to local
.
Another way to initialize the profile on startup, would be to simply add a property in the application.yaml
file, like so:
# spring related settings
spring:
profiles:
active: local
Normally, I would not recommend declaring anything but the true runtime environment, i.e. prod
, as the active profile in your application.yaml
file. That way you don't mistakingly run local
once you deploy your application. Furthermore, this file will be built and minified and there is no easy way of changing this once your source code is live.
Note: The Active Profiles attribute in your IDE's run/debug configuration will take precendent over what is declared in the application.yaml
file.
Therefore, initialize the active profile via the run/debug config, and then set prod
as the default active profile in your application.yaml
file, like so:
# spring related settings
spring:
profiles:
active: prod
Create a properties file that is specific to your local configuration under the resources directory, named application-local.yaml
.
Include the connection details to your local instance of mongo.
application-local.yaml
# local mongodb connection
spring:
data:
mongodb:
database: samplespringapi
host: localhost
port: 27017
One final step is required before firing up our application. We Need to ensure the data being returned is serialized and does not contain nulls.
Serialization is the process of translating a data structure or object state into a format that can be stored or transmitted and reconstructed later.
Further, in order to keep only relevant data in our database, we will exclude any property in the data model that is null
or not set.
To help us accomplish this, we utilize the jackson add-on module in our java project. Start off by adding the following dependencies in our pom.xml
file:
<dependencies>
...
<!--Jackson Datatypes-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
...
</dependencies>
Subsequently, add the correct versions in the < properties >
section:
<properties>
...
<jackson.version>2.13.3</jackson.version>
</properties>
Now, add the following to your application.yaml
file:
application.yaml
spring:
profiles:
active: prod
jackson:
default-property-inclusion: non_null
Furthermore, we can utilize the jackson module to help with serializing JSON properties. Let's take a look at a portion of our StockGeneralResponse.java
class.
StockGeneralResponse.java
public class StockGeneralResponse<T> {
@JsonProperty("response")
private T response;
@JsonProperty("status")
private HttpStatus httpStatus;
@JsonProperty("userMessages")
private UnifiedSet<String> userMessages = UnifiedSet.newSet();
...
}
If you look closely we have annotated each field with @JsonProperty
. The annotation tells Jackson ObjectMapper to map the JSON property name to the annotated Java field's name.
The name of variable is directly used to serialize data. In some cases, you have to rename variables during the serialization/deserialization process. @JsonProperty
is used to tell the serializer how to serial the object.
Granted, the StockGeneralResponse
is an object that we are handling, however, this is more useful when we apply this to a an object model that deals direclty with the raw data (i.e. Stock.java
class).
For example, let's say the data we are about to request has field names that have hypens or underscores in them, like so:
{
"_foo": "bar",
"hello-world": "whatsup"
}
In our object class, we would be able to serialize this data by doing the following:
import java.io.Serializable;
// have the class implement serializable
public class Test implements Serializable {
// annotate the fields with what property to expect from the raw data
@JsonProperty("_foo")
private String foo;
@JsonProperty("hello-world")
private String helloWorld;
}
This allows us to capture the data and then rename them the way we desire, in this scenario, we turn to camelCase. Therefore, the serialized data will look like so:
{
"foo": "bar",
"helloWorld": "whatsup"
}
You can learn more about the settings in this file here: Jackson ObjectMapper
At this point, let's startup 🚀 the API locally!
Utilize the Run/Debug config by pushing the play button in the toolbar of your IDE as shown below:
Run the following commands in the root directory via a bash emulator or the embedded terminal inside IntelliJ:
# maven not configured on $PATH
mvnw.cmd clean install
mvnw.cmd spring-boot:run -Dspring-boot.run.arguments=--spring.profiles.active=local
# maven not configured on $PATH
./mvnw clean install
./mvnw spring-boot:run -Dspring-boot.run.arguments=--spring.profiles.active=local
mvn clean install
mvn spring-boot:run -Dspring-boot.run.arguments=--spring.profiles.active=local
Open up a browser session and navigate to: http://localhost:8080/api/v1/stocks
You should see all the stock entries available via an object of stock objects, like so:
Let's breakdown the series of function calls made once a user calls an endpoint:
Therefore, to implement the getStockBySymbol(String symbol)
function, let's start at the database layer.
Here is a sample of the data stored inside mongo:
{
"symbol": "NIFTY NEXT 50",
"identifier": "NIFTY NEXT 50",
"open": 38759.15,
"dayHigh": 38846.85,
"dayLow": 38684.95,
"lastPrice": 38697.7,
"previousClose": 38678.15,
"change": 19.549999999995634,
"pChange": 0.05,
"yearHigh": 39399.6,
"yearLow": 25614.15,
"totalTradedVolume": 28602611,
"totalTradedValue": 7654580126.95,
"lastUpdateTime": "30-Jun-2021 09:29:14",
"perChange365d": 50.15,
"perChange30d": 3.43
}
If you recall, stock symbol
is also the _id
for each document in the collection. Furthermore, the variable is stored in uppercase. This is something to keep in mind when developing the function inside the service implemnetation class.
Next, we check the repository. The repository will need a method to query the database and return the object that matches the given symbol
.
StockRepository.java
import sample.api.stocks.models.Stock;
@Repository
public interface StockRepository extends MongoRepository<Stock, String> {
Stock findBySymbol(String symbol);
}
Therefore, we add a simple function named findBySymbol
to query the collection for us. To learn more about how this is done, please reference: Query Builder Mechanism
We make our way to the service implementation class. Here we define the business logic for our getStockBySymbol
method.
StockServiceImpl.java
// ask the user to input a string for the symbol they are looking for
public StockGeneralResponse getStockBySymbol(String symbol) {
// the method should automatically capitalize the symbol so that there are no issues regarding case-sensitive querying
Stock stock = stockRepository.findBySymbol(symbol.toUpperCase());
if (stock != null) {
return new StockGeneralResponse(stock, HttpStatus.OK);
} else {
return new StockGeneralResponse(symbol, HttpStatus.NOT_FOUND);
}
}
We skip the service interface since we have already defined our function in the class and go straight to our controller to define the endpoint.
StockController.java
@Operation(summary = "Get a stock by symbol")
// to denote a path variable, we use curly braces like so {...}
@GetMapping(value = "/stocks/{symbol}")
public ResponseEntity<StockGeneralResponse> getStockBySymbol(
// define if the parameter is required or not
@Parameter(description = "A stock symbol", required = true)
// indicate that this parameter is a path variable and define the variable name
@PathVariable() String symbol) {
ResponseEntity<StockGeneralResponse> responseEntity;
// call the service class, passing in the input variable to the function
StockGeneralResponse serviceResponse = stockService.getStockBySymbol(symbol);
responseEntity = new ResponseEntity<>(serviceResponse, serviceResponse.getHttpStatus());
return responseEntity;
}
✅ Now that you understand the basic workflow, let's repeat this for our POST, PUT and DELETE methods!
Here is sequence of calls again:
We can skip the database (db) layer and focus on the repository. If we are going to create a new object in the db, we are going to need a method to save the object to the db.
Just like the findAll()
method, the stock repository has another predefined method called, save()
. This automatically saves the new stock entity and returns the object that was stored in the db, which is what we will return to the client as well. Now let's define our createStock
function.
StockServiceImpl.java
public StockGeneralResponse createStock(Stock stock) {
stockRepository.save(stock);
return new StockGeneralResponse(stock, HttpStatus.CREATED);
}
Further, don't forget to upper-case the symbol
before saving the stock. Since this is going to be the case for each and every object, let's make a change in the object model definition to take this into account, therefore, saving time writing this into our business logic.
Stock.java
// change the setter function to set the incoming symbol to upper-case chars
public void setSymbol(String symbol) {
this.symbol = symbol.toUpperCase();
}
Finally, create an endpoint in the controller class.
StockController.java
@Operation(summary = "Create a new stock")
@PostMapping(value = "/stocks")
public ResponseEntity<StockGeneralResponse> createStock(
@Parameter(description = "New stock object", required = true)
// the annotation @RequestBody indicates an object is required to be passed as an API param to this function
@RequestBody() Stock stock) {
ResponseEntity<StockGeneralResponse> responseEntity;
StockGeneralResponse serviceResponse = stockService.createStock(stock);
responseEntity = new ResponseEntity<>(serviceResponse, serviceResponse.getHttpStatus());
return responseEntity;
}
✅ Try out the next operation!
There is no predefined update method we can make use of in the repository, however the save()
method will allow us to update an object in the db, if one exists.
We will attempt to update the lastPrice
attribute of an existing stock object. In our service implementation we find out if the stock object exists given a certain symbol
. Second, we update only the lastPrice
variable via this function. Lastly, we save the updated stock object back to the database.
StockServiceImpl.java
public StockGeneralResponse updateStock(String symbol, Double lastPrice) throws StockResponseException {
// locate the stock object in the db
Stock currentStock = stockRepository.findBySymbol(symbol.toUpperCase());
// if it exists...
if (currentStock != null) {
// update the price
currentStock.setLastPrice(lastPrice);
// save it
stockRepository.save(currentStock);
// return saved object to user
return new StockGeneralResponse(symbol, HttpStatus.ACCEPTED);
} else {
// if it doesn't exist, throw an exception
throw new StockResponseException("The stock you are trying to update does not exist.");
}
}
If a stock object is not found, then the currentStock
variable would be null and a NullPointerException
would be thrown if the StockResponseException
was not thrown instead.
StockController.java
@Operation(summary = "Update an existing stock by symbol")
@PutMapping(value = "/stocks/{symbol}")
public ResponseEntity<StockGeneralResponse> updateStockBySymbol(
@Parameter(description = "A stock symbol", required = true)
// user needs to specify the symbol of the stock object
@PathVariable() String symbol,
@Parameter(description = "Last Price", required = true)
// the price is passed as a @RequestParam
@RequestParam() Double lastPrice) throws StockResponseException {
ResponseEntity<StockGeneralResponse> responseEntity;
StockGeneralResponse serviceResponse = stockService.updateStock(symbol, lastPrice);
responseEntity = new ResponseEntity<>(serviceResponse, serviceResponse.getHttpStatus());
return responseEntity;
}
✅ Move onto the final endpoint!
In order to delete a stock object, we need to generate a method in the repository first.
StockRepository.java
@Repository
public interface StockRepository extends MongoRepository<Stock, String> {
Stock findBySymbol(String symbol);
void deleteDistinctBySymbol(String symbol);
}
The method has the word distinct in it, which is a keyword defined in the Spring query builder. The query actually results in:
select distinct ...
where x.symbol = ?1
You can always append more attributes to the method to further narrow your search, but it is not needed in our case. For example, you could do:
Function: findBySymbolAndLastPrice
IDE intellisense suggestions:
Which will result in the following SQL:
select distinct ...
where x.symbol = ?1 and x.lastPrice = ?2
Moving on to our service implementation, which is pretty self-explanatory. Traditionally, a DELETE method returns NO_CONTENT
, however, we are trying to give our users the most amount of information. Therefore, we return an HTTP status of 204
or NO_CONTENT
as part of the status
attribute, but we also return the symbol
of the stock object that was deleted just for precaution.
StockServiceImpl.java
public StockGeneralResponse deleteStock(String symbol) {
stockRepository.deleteDistinctBySymbol(symbol.toUpperCase());
return new StockGeneralResponse(symbol, HttpStatus.NO_CONTENT);
}
StockController.java
@Operation(summary = "Delete a stock by symbol")
@DeleteMapping(value = "/stocks/{symbol}")
public ResponseEntity<StockGeneralResponse> deleteStockBySymbol(
@Parameter(description = "A stock symbol", required = true)
@PathVariable() String symbol) {
ResponseEntity<StockGeneralResponse> responseEntity;
StockGeneralResponse serviceResponse = stockService.deleteStock(symbol);
responseEntity = new ResponseEntity<>(serviceResponse, serviceResponse.getHttpStatus());
return responseEntity;
}
We are officially done with all of our five endpoints!
✅ Let's add the configuration needed to startup our Swagger UI on the browser!
Since we already downloaded the dependencies needed for our Swagger UI, all we have left to do is add the configuration class.
Create a class called, SwaggerConfig.java
under the configs pacakge.
SwaggerConfig.java
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI documentation() {
return new OpenAPI()
// name of your API project
.info(new Info().title("Stocks API")
.description("Sample Java Spring Boot API using MongoDB Atlas")
// this version follows semantic versioning
// at the time this codelab was created, the project had been updated to 2.0.0 from 1.0.0
// there are breaking changes from the old code base, therefore we increment the major integer from 1 -> 2
.version("1.0.0")
.contact(new Contact().name("SOME_NAME").url("SOME_URL")))
.externalDocs(new ExternalDocumentation()
.description("Stocks API Documentation")
.url("https://github.com/zarinlo/sample-springboot-api#readme"));
}
}
You can read more about the springfox configurable properties on their website: Springdoc OpenAPI
✅ Move onto to the web security configuration file.
For security reasons, browsers prohibit AJAX calls to resources residing outside the current origin. For example, as you're checking your bank account in one tab, you could have the evil.com website open in another tab. The scripts from evil.com should not be able to make AJAX requests to your bank API (e.g., withdrawing money from your account!) using your credentials.
Cross-origin resource sharing (CORS) is a W3C specification implemented by most browsers that allows you to specify in a flexible way what kind of cross domain requests are authorized, instead of using some less secured and less powerful hacks like IFRAME or JSONP. [excerpt from docs.spring.io]
In Spring, CORS must be processed before Spring Security because the pre-flight request will not contain any cookies. Cookies are not allowed by default to avoid increasing the surface attack of the web application (for example via exposing sensitive user-specific information like CSRF tokens). If you set allowCredentials
property to true
, then pre-flight responses will include the header Access-Control-Allow-Credentials
with value set to true
(see below).
Therefore, if the request does not contain any cookies and Spring Security is first, the request will determine the user is not authenticated (since there are no cookies in the request) and reject it.
The easiest way to ensure that CORS is handled, is to use the CorsRegistry
.
We will enable this by create a CorsConfig.java
class under the configs package.
CorsConfig.java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
// you can allow multiple origins, but this is where our application will run locally
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS");
.allowCredentials(true);
}
}
For further reading:
✅ Go to the next section to finalize the web security configuration file.
We were discussing how cookies are not allowed by default to avoid increasing the surface attack of a web application. A cookie includes sensitive user-specific information like CSRF tokens.
But what does CSRF stand for and what does it mean?
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they're currently authenticated. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker's choosing. If the victim is a normal user, a successful CSRF attack can force the user to perform state changing requests like transferring funds, changing their email address, and so forth. If the victim is an administrative account, CSRF can compromise the entire web application. [excerpt from Open Web Application Security Project® (OWASP)]
Let's see how this is handled in Spring, which by default enables CSRF in Java configurations.
Create another java class under the configs package.
BasicAuthWebSecurityConfiguration.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class BasicAuthWebSecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic();
return http.build();
}
}
For further reading:
🚨 We are almost done, just add the final touches to this project!
We covered how to add Spring properties to application.yaml
files, so now we'll go over Spring actuators. Actuators will be enabled in the base properties file so that we can monitor certain endpoints no matter what profile is activated.
Actuator endpoints help you monitor application level system settings. Spring boot includes a number of built-in endpoints and lets you add your own as well.
Each individual endpoint can be enabled or disabled and exposed (made remotely accessible) over HTTP or JMX. An endpoint is considered to be available when it is both enabled and exposed. The built-in endpoints will only be auto-configured when they are available. Most applications choose exposure via HTTP, where the ID of the endpoint along with a prefix of /actuator
is mapped to a URL. For example, by default, the health endpoint is mapped to /actuator/health
.
To enable spring actuator support, include the following in your pom.xml
file.
<dependencies>
...
<!--Spring-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
...
</dependencies>
As mentioned, by default, actuators can be discovered via the /actuator
endpoint. If you want to override this so that you can access the endpoints via the root path, i.e. /
, we will set the base-path
to a simple forward slash. Furhtermore, we will enable a couple of actuator endpoints, all in our application.yaml
file, like so:
application.yaml
# actuators
management:
endpoints:
web:
base-path: /
exposure:
include: health, metrics, mappings
✅ Next, add in your final build steps.
In your pom.xml
file we need to add the repackage goal for the the spring-boot-maven-plugin. This will package the jar to be an executable so that we can run it as a standalone process. Lastly, add in the maven-compiler-plugin so that we can set the source compiler and target java version for our application. You can set the compiler and target in your IDE as well, but this is another way to do so, that way any CI/CD pipeline will understand how to build the source code.
Positive : Depending on what version of Java you are using, the values for the tag, < configuration >
, may change. They may not always be the same version of java, since this is dependent on what version of the compiler you are using and if it has been updated to use the latest version of java as your project uses.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.2</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
Finally, the < properties >
tag at the top of the pom.xml
file should already have the following attributes. Just make sure the java.version
is set correclty for your project.
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
...
</properties>
✅ Thats it! You are now ready to run your API locally and view the Swagger!
Just a reminder...
# maven not configured on $PATH
mvnw.cmd clean install
mvnw.cmd spring-boot:run -Dspring-boot.run.arguments=--spring.profiles.active=local
# maven not configured on $PATH
./mvnw clean install
./mvnw spring-boot:run -Dspring-boot.run.arguments=--spring.profiles.active=local
mvn clean install
mvn spring-boot:run -Dspring-boot.run.arguments=--spring.profiles.active=local
user
and the password will be found in the Spring Boot console log of your application. The log will say something like: "Using generated security password: b5bef73b-a02b-443d-abb8-84753821f8c3
".v3
stands for the version of OpenAPI, not the version of the API)