A MICRONAUT
WALKS INTO A SPA
MICRONAUT FOR SINGLE-PAGE-APPS
A MICRONAUT WALKS INTO A SPA
ABOUT ME
▸ Zachary Klein is a Senior Software Engineer
at OCI. He has been practicing web
development since 2010 and frontend
development since 2015. He’s a
contributor to both the Grails and
Micronaut frameworks, a conference
speaker and an occasional instructor in
OCI’s training practice.
▸ Zachary’s home base is in St Louis, MO,
along with his wife, Beth, and their three
children.
▸ Brief Introduction to Micronaut
▸ Restful Backends
▸ Gateways
▸ Security with JWT
▸ Building with Gradle
▸ Deploying a SPA in a JAR
A MICRONAUT WALKS INTO A SPA
AGENDA
AND GRAPHQL?
BRIEF INTRODUCTION TO MICRONAUT
▸ Designed with Microservices in mind
▸ Reactive HTTP server built on Netty
▸ AOT (Ahead of Time) Compilation for DI, AOP, and
configuration
▸ Declarative HTTP Client
▸ “Natively” Cloud-Native: service-discovery, LB,
circuit-breakers, tracing, and more
▸ Support for Java, Kotlin and Groovy
A MICRONAUT WALKS INTO A SPA
MICRONAUT
A MICRONAUT WALKS INTO A SPA
MICRONAUT: GETTING STARTED
~ curl -s "https://get.sdkman.io" | bash
~ source "$HOME/.sdkman/bin/sdkman-init.sh"
~ sdk install micronaut
~ mn create-app hello-world
A MICRONAUT WALKS INTO A SPA
MICRONAUT CLI
▸ Language defaults to Java
▸ Use --lang to specify groovy or kotlin
▸ Build tool defaults to Gradle
▸ Use --build to specify maven
▸ Run mn without arguments to enter interactive
mode - includes tab-completion
A MICRONAUT WALKS INTO A SPA
@Controller("/")
class HelloController {
@Get("/hello/{name}")
String hello(String name) {
return "Hello " + name;
}
}
MICRONAUT: CONTROLLERS & CLIENTS
@Client("/")
interface HelloClient {
@Get("/hello/{name}")
String hello(String name);
// Implementation generated
// at compile time
}
A MICRONAUT WALKS INTO A SPA
MICRONAUT: DEPENDENCY INJECTION
@Singleton //Bean definition generated at compile time
class WeatherService {
Integer currentTemp() { //... }
}
@Controller('/weather')
class WeatherController {
@Inject WeatherService weatherService
//DI computed at compile time
@Get("/")
HttpResponse<Integer> currentTemp() {
HttpResponse.ok(weatherService.currentTemp())
}
}
A MICRONAUT WALKS INTO A SPA
MICRONAUT: CLOUD NATIVE
//Lookup client from service-discovery registry
@Client(id="billing", path=“/billing")
interface BillingClient { ... }
//Automatically retry failing calls
@Client("https://api.external.service")
@Retryable(attempts = '3', delay = '5ms')
interface ExternalApiClient { ... }
//Immediately fail after set number of failures
//Begin accepting calls after `reset` interval
@Singleton
@CircuitBreaker(attempts = '5', reset = '300ms')
class MyService { ... }
SERVICE DISCOVERY
RETRYABLE
CIRCUIT BREAKERS
A MICRONAUT WALKS INTO A SPA
MICRONAUT: CLOUD NATIVE
▸ Cloud-Aware Environment Detection
▸ Cloud Provider Integration - AWS, GCP, Spring
Cloud
▸ Metrics & Monitoring
▸ Distributed Configuration
▸ Distributed Tracing
https://angular.io https://vuejs.org https://reactjs.org
~ vue create my-project~ ng new my-dream-app ~ npx create-react-app my-app
RESTFUL BACKENDS
BackendFrontend
REST
{JSON}
BackendFrontend
REST
{JSON}
▸ Declarative Routes via method annotations:
▸ @Get, @Put, @Post, @Delete
▸ JSON binding/rendering via Jackson
▸ Request Arguments via annotations:
▸ @Header, @Body, @CookieValue, @QueryValue
A MICRONAUT WALKS INTO A SPA
MICRONAUT & REST
A MICRONAUT WALKS INTO A SPA
JACKSON: JSON BINDING
public class Author {
private String name;
@JsonSerialize(MySerializer.class)
private Date birthday;
}
@Post(“/")
public HttpResponse<Author> save(
@Body Author author) {
if(bookRepository.save(author)) {
return HttpResponse.ok();
} else {
/* handle error */
}
}
https://www.baeldung.com/jackson-annotations
fetch("http://localhost:8080/
author/", {
method: "POST",
headers: new Headers({
"Content-Type":
"application/json"
}),
body: JSON.stringify({
name: "Author's Name",
birthday: "01/31/1985"
})
})
JAVASCRIPT
A MICRONAUT WALKS INTO A SPA
JACKSON: JSON RENDERING
{
“name": "Title Here”,
"author": {
"name": "Author"
},
"pages": 150,
"tags": [
"tech",
"bestseller"
]
}
@JsonIgnoreProperties({"id", "version"})
public class Book {
private Long id;
private Long version;
@JsonProperty(“name")
private String title;
private Author author;
private Integer pages;
private List<String> tags;
}
@Get("/{id}")
public Book show(Serializable id) {
return bookRepository.get(id);
}
https://www.baeldung.com/jackson-annotations
JSON
A MICRONAUT WALKS INTO A SPA
@Controller("/book")
class BookController {
@Post
HttpResponse<BookDetails> save(@Valid @Body BookDetails bookDetails) { /* .. */}
@Put
HttpResponse<BookDetails> update(@Valid @Body BookDetails bookDetails) { /* .. */}
@Delete("/{id}")
HttpResponse delete(Serializable id) { /* .. */}
@Get(“{?max,offset}")
@Transactional(readOnly = false)
HttpResponse<List<Book>> list(@Nullable Integer max, @Nullable Integer offset) { /* .. */}
@Get("/{id}")
@Transactional(readOnly = true)
HttpResponse<BookDetails> get(Serializable id) { /* .. */}
HttpResponse<Integer> count() { /* .. */}
}
REST CONTROLLER
BOOKCONTROLLER.JAVA
A MICRONAUT WALKS INTO A SPA
import { customHeaders } from "../security/SecurityUtils";
import { composeResourceURL } from "../rest/ClientUtils";
const list = (max, offset, callback, errorCallback) => {
fetch(composeResourceURL(“book","/", { max, offset }), {
method: "GET",
headers: customHeaders()
})
.then(response => response.json())
.then(json => callback(json))
.catch(error => errorCallback(error));
};
const get = (id, callback, errorCallback) => {
/* .. */
};
const save = (resource, callback, errorCallback) => {
/* .. */
};
const update = (resource, callback, errorCallback) => {
/* .. */
};
const remove = (id, callback, errorCallback) => {
/* .. */
};
export default { list, count, get, save, update, remove };
JAVASCRIPT CLIENT
BOOKCLIENT.JS
▸ Framework-agnostic client
▸ Using the fetch API (axios
is another popular option)
▸ Based on callback
functions (async/await is
another option)
▸ customHeaders() function
could return Authorization
headers, tenantId for
multi-tenancy, etc)
A MICRONAUT WALKS INTO A SPA
JAVASCRIPT CLIENT const composeResourceURL = (resource, append, params) => {
if (params !== null) {
append = `${append}${composeParams(params)}`;
}
return `${process.env.SERVER_URL}/${resource}/${append}`;
};
const composeParams = params => {
let paramString = "";
const paramKeys = Object.keys(params);
if (paramKeys.length > 0) {
paramString = "?";
paramKeys.map(key => {
const paramValue = params[key];
if (paramValue !== null && paramValue !== undefined) {
if (paramString !== "?") {
paramString = `${paramString}&`;
}
paramString = `${paramString}${key}=${params[key]}`;
}
});
}
return paramString;
};
export default { composeResourceURL };
CLIENTUTILS.JS
▸ General purpose REST
client functions
▸ Compose restful URL
with resource name,
path, parameters
▸ Compose URL
parameters from object
▸ Resolve Gateway URL
via Node environment
variable (e.g, SERVER_URL)
A MICRONAUT WALKS INTO A SPA
ENABLING CORS
▸ CORS support included in
Micronaut
▸ Disabled by default
▸ Can specify allowed
origins, methods, headers,
max age, and more.
micronaut:
application:
name: my-app
server:
cors:
enabled: true
APPLICATION.YML
▸ An alternative to REST
▸ Server provides a schema that
describes exposed resources
▸ Schema defines queries and mutations
▸ Clients can request only the resources
& fields they want
▸ Client libraries can intelligently merge
queries for efficiency
A MICRONAUT WALKS INTO A SPA
GRAPHQL
https://medium.freecodecamp.org/so-whats-this-graphql-thing-i-keep-hearing-about-baf4d36c20cf?gi=e256cd305c64
A MICRONAUT WALKS INTO A SPA
MICRONAUT & GRAPHQL
▸ micronaut-graphql library - contributed by Marcel Overdijk
▸ Wraps graphql-java and provides a default controller for accepting
queries & mutations
▸ The GraphQL schema still needs to be created, either manually via the
graphql-java API, or an integration library such as GORM for GraphQL
▸ Packages the GraphiQL in-browser IDE for exploring the GraphQL schema
▸ Example Projects: https://github.com/micronaut-projects/micronaut-
graphql/tree/master/examples/
A MICRONAUT WALKS INTO A SPA
MICRONAUT & GRAPHQL
dependencies {
compile “io.micronaut.graphql:micronaut-graphql”
}
BUILD.GRADLE
GATEWAYS
▸ Architectural pattern for microservice-based systems
▸ Expose a single client-facing API (for SPA, mobile, etc)
▸ Minimizing integration points - decoupling
▸ https://microservices.io/patterns/apigateway.html
A MICRONAUT WALKS INTO A SPA
GATEWAYS
Backend
Frontend
BillingMailAnalyticsInventory
Frontend
BillingMailAnalyticsInventory
Frontend
Billing
Frontend
MailAnalyticsInventory
Gateway
▸ Version by URL: @Get("/v1/user/profile")
▸ Using config property:
@Value("${core.api.version}")
String version
@Get("/${version}/user/profile")
‣ Client-facing versioning can be separate from versioning
within the microservice architecture
A MICRONAUT WALKS INTO A SPA
VERSIONING AN API
core:
api:
version: v1
APPLICATION.YML
BillingMailAnalyticsInventory
Frontend
Gateway
V1
Gateway
V2
BillingMailAnalyticsInventory
Frontend
Gateway
V1
Gateway
V2
▸ Partition your API
▸ Support different
client experiences/
functions (e.g, admin
vs customer)
A MICRONAUT WALKS INTO A SPA
MULTIPLE GATEWAYS
Billing
Web
MailAnalyticsInventory
Gateway
Admin Web
Admin
Gateway
Mobile
Mobile
Gateway
A MICRONAUT WALKS INTO A SPA
MICRONAUT PETSTORE
https://github.com/micronaut-projects/micronaut-examples/tree/master/petstore
SECURITY WITH JWT
A MICRONAUT WALKS INTO A SPA
JWTI: JSON WEB TOKEN
‣ Open standard for representing
claims securely between two parties
‣ Tokens can be signed with either a
secret or public/private key
‣ Standard approach for stateless
authentication
‣ Ideal for transmitting authentication
& authorization data between
microservices and single-page-apps
A MICRONAUT WALKS INTO A SPA
MICRONAUT SECURITY
▸ Core Micronaut Library - supports JWT, Session, Basic Auth
▸ Annotation-based API & config-based URL mappings
▸ Support for token propagation
▸ Supports RFC 6750 Bearer Token 
▸ JWTs can be read from cookie
dependencies {
compile "io.micronaut:micronaut-security-jwt"
}
micronaut:
security:
enabled: true
token:
jwt:
enabled: true
signatures:
secret:
generator:
secret: changeMe
APPLICATION.YML
BUILD.GRADLE
A MICRONAUT WALKS INTO A SPA
@SECURED ANNOTATION
▸ @Secured annotation applied
to controllers and methods
▸ All routes blocked by default
▸ Can require authentication
and/or authorization (role-
based)
▸ Alternative: JSR-250 security
annotations are also
supported: @PermitAll,
@RolesAllowed, @DenyAll
import java.security.Principal;
@Secured("isAuthenticated()")
@Controller("/")
public class HomeController {
@Get("/")
String index(Principal principal) {
return principal.getName();
}
@Secured({"ROLE_ADMIN", "ROLE_X"})
@Get("/classified")
String classified() {
return /* REDACTED */;
}
}
A MICRONAUT WALKS INTO A SPA
‣ Unauthorized request is made to API
‣ Responds with 401
‣ Client POSTs to login endpoint
‣ Server responds with JWT
‣ Client includes access token in the
Authorization header for subsequent
requests
‣ Server validates the incoming token
‣ If authorized, server responds with
resource
MICRONAUT JWT SECURITY
A MICRONAUT WALKS INTO A SPA
‣ POST credentials to the /
login endpoint
‣ Retrieve access token
from JWT and store in
application state, a
cookie, or local storage*
JAVASCRIPT JWT SECURITY const login = (credentials, callback, errorCallback) => {
fetch(`${process.env.SERVER_URL}/login`, {
method: "POST",
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
body: JSON.stringify(credentials)
})
.then(response => {
if (response.ok) {
return response.json();
} else {
throw Error(response.statusText);
}
})
.then(json => callback(json))
.catch(error => errorCallback(error));
};
export default { login };
AUTHCLIENT.JS
import AuthClient from “./AuthClient";
AuthClient.login(
{ username: this.username, password: this.password },
json => /* handle login success */,
error => console.error(error)
);
*Local storage is not inherently secure:
https://www.rdegges.com/2018/please-stop-using-local-storage/
A MICRONAUT WALKS INTO A SPA
‣ Use custom Headers
object to include access
token (if using Bearer)
JAVASCRIPT JWT SECURITY
const securedHeaders = () => {
return new Headers({
Authorization: `Bearer ${token()}`,
"Content-Type": "application/json"
});
};
const token = () => //retrieve token from store;
export default { securedHeaders };
SECURITYUTILS.JS
import { securedHeaders } from "./SecurityUtils";
fetch(`${process.env.SERVER_URL}/home`, {
method: "GET",
headers: securedHeaders()
})
.then(response => response.text())
.then(text => (this.content = text))
.catch(error => {
console.log("Connection failure: ", error);
});
A MICRONAUT WALKS INTO A SPA
MICRONAUT SECURITY GUIDES
https://guides.micronaut.io/tags/security.html
BUILDING SPAS WITH GRADLE
MULTIPROJECT BUILDS
A MICRONAUT WALKS INTO A SPA
JAVASCRIPT APP
BackendFrontend
include "client", “server"
SETTINGS.GRADLE
GRADLE NODE PLUGIN
A MICRONAUT WALKS INTO A SPA
plugins {
id "com.github.node-gradle.node" version "1.3.0"
}
node {
version = '10.15.0' // https://nodejs.org/en/
yarnVersion = '1.13.0' // https://yarnpkg.com/en/
download = true
}
task start(type: YarnTask, dependsOn: 'yarn') {
group = 'application'
description = 'Run the client app'
args = ['run', 'start']
}
task build(type: YarnTask, dependsOn: 'yarn') {
group = 'build'
description = 'Build the client bundle'
args = ['run', 'build']
}
task test(type: YarnTask, dependsOn: 'yarn') {
group = 'verification'
description = 'Run the client tests'
args = ['run', 'test']
}
task eject(type: YarnTask, dependsOn: 'yarn') {
//...
}
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
//...
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
~ gradle start //yarn start
Server running on http://localhost:3000
BUILD.GRADLE PACKAGE.JSON
DEPLOYING A SPA IN A JAR
▸ Serve the Single-Page-App from within an executable JAR
file
▸ Removes need for a separate static server
▸ SPA can be packaged with its gateway
▸ Simplifies deployment of small apps and/or IoT
deployments
A MICRONAUT WALKS INTO A SPA
DEPLOYING A SPA IN A JAR
~ gradle shadowJar
BUILD SUCCESSFUL in 1s
DEPLOYMENT
A MICRONAUT WALKS INTO A SPA
JAVASCRIPT APP
~ yarn build
Creating a production build...
Compiled successfully.
DEPLOYMENT
A MICRONAUT WALKS INTO A SPA
JAVASCRIPT APP
BackendFrontend
COMBINED BUILD
A MICRONAUT WALKS INTO A SPA
~ java -jar build/libs/server-all.jar
Server Running: http://localhost:8080
STATIC ASSETS IN MICRONAUT
A MICRONAUT WALKS INTO A SPA
▸ Disabled by default
▸
▸
▸ List of paths, starting with classpath: or file:
▸
▸ The path from which resources should be served
micronaut.router.static-resources.*.enabled
micronaut.router.static-resources.*.paths
micronaut.router.static-resources.*.mapping
STATIC ASSETS IN MICRONAUT
A MICRONAUT WALKS INTO A SPA
micronaut:
router:
static-resources:
default:
enabled: true
mapping: "/**"
paths: "classpath:public"
local:
enabled: true
mapping: “/local/**"
paths: “file:path/to/files“
APPLICATION.YML
COMBINED BUILD
A MICRONAUT WALKS INTO A SPA
//Copy production SPA resources into server build directory
task copyClientResources(dependsOn: ':client:build') {
group = 'build'
description = 'Copy client resources into server'
}
copyClientResources.doFirst {
copy {
from project(':client').buildDir.absolutePath
into "${project(':server').buildDir}/resources/main/public"
}
}
//Build a single executable JAR with embedded SPA resources
task assembleServerAndClient(
dependsOn: ['copyClientResources', ':server:shadowJar']) {
group = 'build'
description = 'Build combined server & client JAR'
}
BUILD.GRADLE
COMBINED BUILD
A MICRONAUT WALKS INTO A SPA
~ java -jar build/libs/server-all.jar
Server Running: http://localhost:8080
MICRONAUT SINGLE-PAGE-APP GUIDE
A MICRONAUT WALKS INTO A SPA
https://guides.micronaut.io/micronaut-spa-react/guide/index.htmlNEW!
A MICRONAUT WALKS INTO A SPA
MICRONAUT PETSTORE
https://github.com/micronaut-projects/micronaut-examples/tree/master/petstore
A MICRONAUT WALKS INTO A SPA
LEARN MORE ABOUT OCI EVENTS AND TRAINING
Events:
‣ objectcomputing.com/events
Training:
‣ objectcomputing.com/training
‣ grailstraining.com
‣ micronauttraining.com
COME SEE US
AT OUR BOOTH!
A MICRONAUT WALKS INTO A SPA
THANK YOU@ZacharyAKlein kleinz@objectcomputing.com

Micronaut For Single Page Apps

  • 1.
    A MICRONAUT WALKS INTOA SPA MICRONAUT FOR SINGLE-PAGE-APPS
  • 2.
    A MICRONAUT WALKSINTO A SPA ABOUT ME ▸ Zachary Klein is a Senior Software Engineer at OCI. He has been practicing web development since 2010 and frontend development since 2015. He’s a contributor to both the Grails and Micronaut frameworks, a conference speaker and an occasional instructor in OCI’s training practice. ▸ Zachary’s home base is in St Louis, MO, along with his wife, Beth, and their three children.
  • 3.
    ▸ Brief Introductionto Micronaut ▸ Restful Backends ▸ Gateways ▸ Security with JWT ▸ Building with Gradle ▸ Deploying a SPA in a JAR A MICRONAUT WALKS INTO A SPA AGENDA AND GRAPHQL?
  • 4.
  • 5.
    ▸ Designed withMicroservices in mind ▸ Reactive HTTP server built on Netty ▸ AOT (Ahead of Time) Compilation for DI, AOP, and configuration ▸ Declarative HTTP Client ▸ “Natively” Cloud-Native: service-discovery, LB, circuit-breakers, tracing, and more ▸ Support for Java, Kotlin and Groovy A MICRONAUT WALKS INTO A SPA MICRONAUT
  • 6.
    A MICRONAUT WALKSINTO A SPA MICRONAUT: GETTING STARTED ~ curl -s "https://get.sdkman.io" | bash ~ source "$HOME/.sdkman/bin/sdkman-init.sh" ~ sdk install micronaut ~ mn create-app hello-world
  • 7.
    A MICRONAUT WALKSINTO A SPA MICRONAUT CLI ▸ Language defaults to Java ▸ Use --lang to specify groovy or kotlin ▸ Build tool defaults to Gradle ▸ Use --build to specify maven ▸ Run mn without arguments to enter interactive mode - includes tab-completion
  • 8.
    A MICRONAUT WALKSINTO A SPA @Controller("/") class HelloController { @Get("/hello/{name}") String hello(String name) { return "Hello " + name; } } MICRONAUT: CONTROLLERS & CLIENTS @Client("/") interface HelloClient { @Get("/hello/{name}") String hello(String name); // Implementation generated // at compile time }
  • 9.
    A MICRONAUT WALKSINTO A SPA MICRONAUT: DEPENDENCY INJECTION @Singleton //Bean definition generated at compile time class WeatherService { Integer currentTemp() { //... } } @Controller('/weather') class WeatherController { @Inject WeatherService weatherService //DI computed at compile time @Get("/") HttpResponse<Integer> currentTemp() { HttpResponse.ok(weatherService.currentTemp()) } }
  • 10.
    A MICRONAUT WALKSINTO A SPA MICRONAUT: CLOUD NATIVE //Lookup client from service-discovery registry @Client(id="billing", path=“/billing") interface BillingClient { ... } //Automatically retry failing calls @Client("https://api.external.service") @Retryable(attempts = '3', delay = '5ms') interface ExternalApiClient { ... } //Immediately fail after set number of failures //Begin accepting calls after `reset` interval @Singleton @CircuitBreaker(attempts = '5', reset = '300ms') class MyService { ... } SERVICE DISCOVERY RETRYABLE CIRCUIT BREAKERS
  • 11.
    A MICRONAUT WALKSINTO A SPA MICRONAUT: CLOUD NATIVE ▸ Cloud-Aware Environment Detection ▸ Cloud Provider Integration - AWS, GCP, Spring Cloud ▸ Metrics & Monitoring ▸ Distributed Configuration ▸ Distributed Tracing
  • 13.
    https://angular.io https://vuejs.org https://reactjs.org ~vue create my-project~ ng new my-dream-app ~ npx create-react-app my-app
  • 14.
  • 15.
  • 16.
  • 17.
    ▸ Declarative Routesvia method annotations: ▸ @Get, @Put, @Post, @Delete ▸ JSON binding/rendering via Jackson ▸ Request Arguments via annotations: ▸ @Header, @Body, @CookieValue, @QueryValue A MICRONAUT WALKS INTO A SPA MICRONAUT & REST
  • 18.
    A MICRONAUT WALKSINTO A SPA JACKSON: JSON BINDING public class Author { private String name; @JsonSerialize(MySerializer.class) private Date birthday; } @Post(“/") public HttpResponse<Author> save( @Body Author author) { if(bookRepository.save(author)) { return HttpResponse.ok(); } else { /* handle error */ } } https://www.baeldung.com/jackson-annotations fetch("http://localhost:8080/ author/", { method: "POST", headers: new Headers({ "Content-Type": "application/json" }), body: JSON.stringify({ name: "Author's Name", birthday: "01/31/1985" }) }) JAVASCRIPT
  • 19.
    A MICRONAUT WALKSINTO A SPA JACKSON: JSON RENDERING { “name": "Title Here”, "author": { "name": "Author" }, "pages": 150, "tags": [ "tech", "bestseller" ] } @JsonIgnoreProperties({"id", "version"}) public class Book { private Long id; private Long version; @JsonProperty(“name") private String title; private Author author; private Integer pages; private List<String> tags; } @Get("/{id}") public Book show(Serializable id) { return bookRepository.get(id); } https://www.baeldung.com/jackson-annotations JSON
  • 20.
    A MICRONAUT WALKSINTO A SPA @Controller("/book") class BookController { @Post HttpResponse<BookDetails> save(@Valid @Body BookDetails bookDetails) { /* .. */} @Put HttpResponse<BookDetails> update(@Valid @Body BookDetails bookDetails) { /* .. */} @Delete("/{id}") HttpResponse delete(Serializable id) { /* .. */} @Get(“{?max,offset}") @Transactional(readOnly = false) HttpResponse<List<Book>> list(@Nullable Integer max, @Nullable Integer offset) { /* .. */} @Get("/{id}") @Transactional(readOnly = true) HttpResponse<BookDetails> get(Serializable id) { /* .. */} HttpResponse<Integer> count() { /* .. */} } REST CONTROLLER BOOKCONTROLLER.JAVA
  • 21.
    A MICRONAUT WALKSINTO A SPA import { customHeaders } from "../security/SecurityUtils"; import { composeResourceURL } from "../rest/ClientUtils"; const list = (max, offset, callback, errorCallback) => { fetch(composeResourceURL(“book","/", { max, offset }), { method: "GET", headers: customHeaders() }) .then(response => response.json()) .then(json => callback(json)) .catch(error => errorCallback(error)); }; const get = (id, callback, errorCallback) => { /* .. */ }; const save = (resource, callback, errorCallback) => { /* .. */ }; const update = (resource, callback, errorCallback) => { /* .. */ }; const remove = (id, callback, errorCallback) => { /* .. */ }; export default { list, count, get, save, update, remove }; JAVASCRIPT CLIENT BOOKCLIENT.JS ▸ Framework-agnostic client ▸ Using the fetch API (axios is another popular option) ▸ Based on callback functions (async/await is another option) ▸ customHeaders() function could return Authorization headers, tenantId for multi-tenancy, etc)
  • 22.
    A MICRONAUT WALKSINTO A SPA JAVASCRIPT CLIENT const composeResourceURL = (resource, append, params) => { if (params !== null) { append = `${append}${composeParams(params)}`; } return `${process.env.SERVER_URL}/${resource}/${append}`; }; const composeParams = params => { let paramString = ""; const paramKeys = Object.keys(params); if (paramKeys.length > 0) { paramString = "?"; paramKeys.map(key => { const paramValue = params[key]; if (paramValue !== null && paramValue !== undefined) { if (paramString !== "?") { paramString = `${paramString}&`; } paramString = `${paramString}${key}=${params[key]}`; } }); } return paramString; }; export default { composeResourceURL }; CLIENTUTILS.JS ▸ General purpose REST client functions ▸ Compose restful URL with resource name, path, parameters ▸ Compose URL parameters from object ▸ Resolve Gateway URL via Node environment variable (e.g, SERVER_URL)
  • 23.
    A MICRONAUT WALKSINTO A SPA ENABLING CORS ▸ CORS support included in Micronaut ▸ Disabled by default ▸ Can specify allowed origins, methods, headers, max age, and more. micronaut: application: name: my-app server: cors: enabled: true APPLICATION.YML
  • 24.
    ▸ An alternativeto REST ▸ Server provides a schema that describes exposed resources ▸ Schema defines queries and mutations ▸ Clients can request only the resources & fields they want ▸ Client libraries can intelligently merge queries for efficiency A MICRONAUT WALKS INTO A SPA GRAPHQL https://medium.freecodecamp.org/so-whats-this-graphql-thing-i-keep-hearing-about-baf4d36c20cf?gi=e256cd305c64
  • 25.
    A MICRONAUT WALKSINTO A SPA MICRONAUT & GRAPHQL
  • 26.
    ▸ micronaut-graphql library- contributed by Marcel Overdijk ▸ Wraps graphql-java and provides a default controller for accepting queries & mutations ▸ The GraphQL schema still needs to be created, either manually via the graphql-java API, or an integration library such as GORM for GraphQL ▸ Packages the GraphiQL in-browser IDE for exploring the GraphQL schema ▸ Example Projects: https://github.com/micronaut-projects/micronaut- graphql/tree/master/examples/ A MICRONAUT WALKS INTO A SPA MICRONAUT & GRAPHQL dependencies { compile “io.micronaut.graphql:micronaut-graphql” } BUILD.GRADLE
  • 27.
  • 28.
    ▸ Architectural patternfor microservice-based systems ▸ Expose a single client-facing API (for SPA, mobile, etc) ▸ Minimizing integration points - decoupling ▸ https://microservices.io/patterns/apigateway.html A MICRONAUT WALKS INTO A SPA GATEWAYS
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
    ▸ Version byURL: @Get("/v1/user/profile") ▸ Using config property: @Value("${core.api.version}") String version @Get("/${version}/user/profile") ‣ Client-facing versioning can be separate from versioning within the microservice architecture A MICRONAUT WALKS INTO A SPA VERSIONING AN API core: api: version: v1 APPLICATION.YML
  • 34.
  • 35.
  • 36.
    ▸ Partition yourAPI ▸ Support different client experiences/ functions (e.g, admin vs customer) A MICRONAUT WALKS INTO A SPA MULTIPLE GATEWAYS Billing Web MailAnalyticsInventory Gateway Admin Web Admin Gateway Mobile Mobile Gateway
  • 37.
    A MICRONAUT WALKSINTO A SPA MICRONAUT PETSTORE https://github.com/micronaut-projects/micronaut-examples/tree/master/petstore
  • 38.
  • 39.
    A MICRONAUT WALKSINTO A SPA JWTI: JSON WEB TOKEN ‣ Open standard for representing claims securely between two parties ‣ Tokens can be signed with either a secret or public/private key ‣ Standard approach for stateless authentication ‣ Ideal for transmitting authentication & authorization data between microservices and single-page-apps
  • 40.
    A MICRONAUT WALKSINTO A SPA MICRONAUT SECURITY ▸ Core Micronaut Library - supports JWT, Session, Basic Auth ▸ Annotation-based API & config-based URL mappings ▸ Support for token propagation ▸ Supports RFC 6750 Bearer Token  ▸ JWTs can be read from cookie dependencies { compile "io.micronaut:micronaut-security-jwt" } micronaut: security: enabled: true token: jwt: enabled: true signatures: secret: generator: secret: changeMe APPLICATION.YML BUILD.GRADLE
  • 41.
    A MICRONAUT WALKSINTO A SPA @SECURED ANNOTATION ▸ @Secured annotation applied to controllers and methods ▸ All routes blocked by default ▸ Can require authentication and/or authorization (role- based) ▸ Alternative: JSR-250 security annotations are also supported: @PermitAll, @RolesAllowed, @DenyAll import java.security.Principal; @Secured("isAuthenticated()") @Controller("/") public class HomeController { @Get("/") String index(Principal principal) { return principal.getName(); } @Secured({"ROLE_ADMIN", "ROLE_X"}) @Get("/classified") String classified() { return /* REDACTED */; } }
  • 42.
    A MICRONAUT WALKSINTO A SPA ‣ Unauthorized request is made to API ‣ Responds with 401 ‣ Client POSTs to login endpoint ‣ Server responds with JWT ‣ Client includes access token in the Authorization header for subsequent requests ‣ Server validates the incoming token ‣ If authorized, server responds with resource MICRONAUT JWT SECURITY
  • 43.
    A MICRONAUT WALKSINTO A SPA ‣ POST credentials to the / login endpoint ‣ Retrieve access token from JWT and store in application state, a cookie, or local storage* JAVASCRIPT JWT SECURITY const login = (credentials, callback, errorCallback) => { fetch(`${process.env.SERVER_URL}/login`, { method: "POST", headers: new Headers({ Accept: "application/json", "Content-Type": "application/json", }), body: JSON.stringify(credentials) }) .then(response => { if (response.ok) { return response.json(); } else { throw Error(response.statusText); } }) .then(json => callback(json)) .catch(error => errorCallback(error)); }; export default { login }; AUTHCLIENT.JS import AuthClient from “./AuthClient"; AuthClient.login( { username: this.username, password: this.password }, json => /* handle login success */, error => console.error(error) ); *Local storage is not inherently secure: https://www.rdegges.com/2018/please-stop-using-local-storage/
  • 44.
    A MICRONAUT WALKSINTO A SPA ‣ Use custom Headers object to include access token (if using Bearer) JAVASCRIPT JWT SECURITY const securedHeaders = () => { return new Headers({ Authorization: `Bearer ${token()}`, "Content-Type": "application/json" }); }; const token = () => //retrieve token from store; export default { securedHeaders }; SECURITYUTILS.JS import { securedHeaders } from "./SecurityUtils"; fetch(`${process.env.SERVER_URL}/home`, { method: "GET", headers: securedHeaders() }) .then(response => response.text()) .then(text => (this.content = text)) .catch(error => { console.log("Connection failure: ", error); });
  • 45.
    A MICRONAUT WALKSINTO A SPA MICRONAUT SECURITY GUIDES https://guides.micronaut.io/tags/security.html
  • 46.
  • 47.
    MULTIPROJECT BUILDS A MICRONAUTWALKS INTO A SPA JAVASCRIPT APP BackendFrontend include "client", “server" SETTINGS.GRADLE
  • 48.
    GRADLE NODE PLUGIN AMICRONAUT WALKS INTO A SPA plugins { id "com.github.node-gradle.node" version "1.3.0" } node { version = '10.15.0' // https://nodejs.org/en/ yarnVersion = '1.13.0' // https://yarnpkg.com/en/ download = true } task start(type: YarnTask, dependsOn: 'yarn') { group = 'application' description = 'Run the client app' args = ['run', 'start'] } task build(type: YarnTask, dependsOn: 'yarn') { group = 'build' description = 'Build the client bundle' args = ['run', 'build'] } task test(type: YarnTask, dependsOn: 'yarn') { group = 'verification' description = 'Run the client tests' args = ['run', 'test'] } task eject(type: YarnTask, dependsOn: 'yarn') { //... } { "name": "client", "version": "0.1.0", "private": true, "dependencies": { //... }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, ~ gradle start //yarn start Server running on http://localhost:3000 BUILD.GRADLE PACKAGE.JSON
  • 49.
  • 50.
    ▸ Serve theSingle-Page-App from within an executable JAR file ▸ Removes need for a separate static server ▸ SPA can be packaged with its gateway ▸ Simplifies deployment of small apps and/or IoT deployments A MICRONAUT WALKS INTO A SPA DEPLOYING A SPA IN A JAR
  • 51.
    ~ gradle shadowJar BUILDSUCCESSFUL in 1s DEPLOYMENT A MICRONAUT WALKS INTO A SPA JAVASCRIPT APP ~ yarn build Creating a production build... Compiled successfully.
  • 52.
    DEPLOYMENT A MICRONAUT WALKSINTO A SPA JAVASCRIPT APP BackendFrontend
  • 53.
    COMBINED BUILD A MICRONAUTWALKS INTO A SPA ~ java -jar build/libs/server-all.jar Server Running: http://localhost:8080
  • 54.
    STATIC ASSETS INMICRONAUT A MICRONAUT WALKS INTO A SPA ▸ Disabled by default ▸ ▸ ▸ List of paths, starting with classpath: or file: ▸ ▸ The path from which resources should be served micronaut.router.static-resources.*.enabled micronaut.router.static-resources.*.paths micronaut.router.static-resources.*.mapping
  • 55.
    STATIC ASSETS INMICRONAUT A MICRONAUT WALKS INTO A SPA micronaut: router: static-resources: default: enabled: true mapping: "/**" paths: "classpath:public" local: enabled: true mapping: “/local/**" paths: “file:path/to/files“ APPLICATION.YML
  • 56.
    COMBINED BUILD A MICRONAUTWALKS INTO A SPA //Copy production SPA resources into server build directory task copyClientResources(dependsOn: ':client:build') { group = 'build' description = 'Copy client resources into server' } copyClientResources.doFirst { copy { from project(':client').buildDir.absolutePath into "${project(':server').buildDir}/resources/main/public" } } //Build a single executable JAR with embedded SPA resources task assembleServerAndClient( dependsOn: ['copyClientResources', ':server:shadowJar']) { group = 'build' description = 'Build combined server & client JAR' } BUILD.GRADLE
  • 57.
    COMBINED BUILD A MICRONAUTWALKS INTO A SPA ~ java -jar build/libs/server-all.jar Server Running: http://localhost:8080
  • 58.
    MICRONAUT SINGLE-PAGE-APP GUIDE AMICRONAUT WALKS INTO A SPA https://guides.micronaut.io/micronaut-spa-react/guide/index.htmlNEW!
  • 59.
    A MICRONAUT WALKSINTO A SPA MICRONAUT PETSTORE https://github.com/micronaut-projects/micronaut-examples/tree/master/petstore
  • 60.
    A MICRONAUT WALKSINTO A SPA LEARN MORE ABOUT OCI EVENTS AND TRAINING Events: ‣ objectcomputing.com/events Training: ‣ objectcomputing.com/training ‣ grailstraining.com ‣ micronauttraining.com COME SEE US AT OUR BOOTH!
  • 61.
    A MICRONAUT WALKSINTO A SPA THANK YOU@ZacharyAKlein kleinz@objectcomputing.com