October 10, 2020

Deploy Spring Boot & Angular App to Heroku

In this article, we are going to see how to Deploy Spring Boot and Angular application to Heroku. I am going to take the Reddit Clone application I have built as an example for this tutorial.

You can access the source code of Backend and Frontend apps

If you are a visual learner like me, you can check out the video version of this tutorial below:

First, we are going to deploy our Spring Boot REST API and then we deploy the Angular Application to Heroku.

Preparing our Spring Boot Application for Production

First let’s make changes to our Spring Boot app to make it production ready.

Using PostgreSQL Database for Production

We are going to use PostgreSQL as our production database, we are using this because Heroku provides nice out of the box support for PostgreSQL, we can also use MySQL but we have to provide the Credit Card information. So I prefer to go ahead with PostgreSQL DB.

To handle MySQL Database for Local Development, and PostgreSQL for the Production environment, we can make use of the concept of Profiles in Spring Boot, where you can provide a different set of configuration properties for each profile.

In our case, for Production usage, we are going to use PostgreSQL Database and while developing we will be using MySQL, so I am going to create two properties files inside the src/main/resources folder

  • application-local.properties
  • application-prod.properties

This will give us access to 2 profiles – local and prod.

application-local.properties

############# Database Properties #######################################
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring-reddit-clone?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
spring.datasource.username=root
spring.datasource.password=mysql
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

application-prod.properties

############# Database Properties ##################
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.datasource.driverClassName=org.postgresql.Driver
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false

Notice, that I am not providing the Datasource URL, Username and Password details for the PostgreSQL DB because, this will be automatically injected by Heroku, we will have a look at it shortly.

Lastly, I am going to add the PostgreSQL Java Driver dependency to our project, so that Spring Boot automatically configures the PostgreSQL driver for us.

<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
  <version>42.2.16</version>
</dependency>

NOTE:

If you are storing User information in your application using a table called ‘users’, you can face a problem because the keyword users is reserved by PostgreSQL Database, so make sure that your table name which stores the user information is named anything other than users

In our example, I am going to use a different name for table which is storing the User Entity.

package com.programming.techie.springredditclone.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.time.Instant;

import static javax.persistence.GenerationType.IDENTITY;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "reddit_users")
public class User {
    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long userId;
    @NotBlank(message = "Username is required")
    private String username;
    @NotBlank(message = "Password is required")
    private String password;
    @Email
    @NotEmpty(message = "Email is required")
    private String email;
    private Instant created;
    private boolean enabled;
}

Replace localhost with App Name

If you have any references to localhost in your application, make sure to replace it with the Heroku URL (see the section Login to Heroku through CLI)

In our application we have the Account Activation URL hardcoded into the class file, let’s try to pull this out into a properties file, later we will use the Configuration Variables in Heroku to pass them dynamically at run-time.

Usually, in spring, if you want to inject a property into your class, you will use the @Value annotation, there is nothing wrong in using this annotation if you are injecting only one property, but if you are working on a bigger application, then you will quickly loose track of all the property files which you are injecting into your classes at run time.

To make this configuration easier, Spring Boot provides us with an annotation @ConfigurationProperties where you can centralize all the logic to define the properties which are injected into your classes.

AppConfig.java

package com.programming.techie.springredditclone.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

import javax.validation.constraints.NotNull;

@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "app")
public class AppConfig {
    @NotNull
    private String appUrl;

    public String getAppUrl() {
        return appUrl;
    }
}

As mentioned below, although this class contains only one property, this can become a single point of access to define all your property files.

We can just inject this AppConfig class like any other class, in our case, we want to use this in the AuthService class where we are hardcoding with http://localhost:8080 URL

AuthService.java

package com.programming.techie.springredditclone.service;

import com.programming.techie.springredditclone.config.AppConfig;
import com.programming.techie.springredditclone.dto.AuthenticationResponse;
import com.programming.techie.springredditclone.dto.LoginRequest;
import com.programming.techie.springredditclone.dto.RefreshTokenRequest;
import com.programming.techie.springredditclone.dto.RegisterRequest;
import com.programming.techie.springredditclone.exceptions.SpringRedditException;
import com.programming.techie.springredditclone.model.NotificationEmail;
import com.programming.techie.springredditclone.model.User;
import com.programming.techie.springredditclone.model.VerificationToken;
import com.programming.techie.springredditclone.repository.UserRepository;
import com.programming.techie.springredditclone.repository.VerificationTokenRepository;
import com.programming.techie.springredditclone.security.JwtProvider;
import lombok.AllArgsConstructor;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.Optional;
import java.util.UUID;

@Service
@AllArgsConstructor
@Transactional
public class AuthService {

    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    private final VerificationTokenRepository verificationTokenRepository;
    private final MailService mailService;
    private final AuthenticationManager authenticationManager;
    private final JwtProvider jwtProvider;
    private final RefreshTokenService refreshTokenService;
    private final AppConfig appConfig;


    public void signup(RegisterRequest registerRequest) {
        User user = new User();
        user.setUsername(registerRequest.getUsername());
        user.setEmail(registerRequest.getEmail());
        user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
        user.setCreated(Instant.now());
        user.setEnabled(false);

        userRepository.save(user);

        String token = generateVerificationToken(user);
        mailService.sendMail(new NotificationEmail("Please Activate your Account",
                user.getEmail(), "Thank you for signing up to Spring Reddit, " +
                "please click on the below url to activate your account : " +
                appConfig.getAppUrl() + "/api/auth/accountVerification/" + token));
    }

    @Transactional(readOnly = true)
    public User getCurrentUser() {
        org.springframework.security.core.userdetails.User principal = (org.springframework.security.core.userdetails.User) SecurityContextHolder.
                getContext().getAuthentication().getPrincipal();
        return userRepository.findByUsername(principal.getUsername())
                .orElseThrow(() -> new UsernameNotFoundException("User name not found - " + principal.getUsername()));
    }

    private void fetchUserAndEnable(VerificationToken verificationToken) {
        String username = verificationToken.getUser().getUsername();
        User user = userRepository.findByUsername(username).orElseThrow(() -> new SpringRedditException("User not found with name - " + username));
        user.setEnabled(true);
        userRepository.save(user);
    }

    private String generateVerificationToken(User user) {
        String token = UUID.randomUUID().toString();
        VerificationToken verificationToken = new VerificationToken();
        verificationToken.setToken(token);
        verificationToken.setUser(user);

        verificationTokenRepository.save(verificationToken);
        return token;
    }

    public void verifyAccount(String token) {
        Optional<VerificationToken> verificationToken = verificationTokenRepository.findByToken(token);
        fetchUserAndEnable(verificationToken.orElseThrow(() -> new SpringRedditException("Invalid Token")));
    }

    public AuthenticationResponse login(LoginRequest loginRequest) {
        Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(),
                loginRequest.getPassword()));
        SecurityContextHolder.getContext().setAuthentication(authenticate);
        String token = jwtProvider.generateToken(authenticate);
        return AuthenticationResponse.builder()
                .authenticationToken(token)
                .refreshToken(refreshTokenService.generateRefreshToken().getToken())
                .expiresAt(Instant.now().plusMillis(jwtProvider.getJwtExpirationInMillis()))
                .username(loginRequest.getUsername())
                .build();
    }

    public AuthenticationResponse refreshToken(RefreshTokenRequest refreshTokenRequest) {
        refreshTokenService.validateRefreshToken(refreshTokenRequest.getRefreshToken());
        String token = jwtProvider.generateTokenWithUserName(refreshTokenRequest.getUsername());
        return AuthenticationResponse.builder()
                .authenticationToken(token)
                .refreshToken(refreshTokenRequest.getRefreshToken())
                .expiresAt(Instant.now().plusMillis(jwtProvider.getJwtExpirationInMillis()))
                .username(refreshTokenRequest.getUsername())
                .build();
    }

    public boolean isLoggedIn() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated();
    }
}

Now we have to define the property also in our application.properties file. When you are running the application in local, we have to use the http://localhost:8080 URL, so I am going to create a property app.url inside the application-local.properties file

application-local.properties

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring-reddit-clone?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
spring.datasource.username=root
spring.datasource.password=mysql
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
################## Application URL ###############
app.url=http://localhost:8080

For the application-prod.properties we need the URL of the Heroku instance, this can be injected at runtime using the Heroku Environment Variables, so there is no need to mention the app.url property inside the application-prod.properties file. We will see how to inject this variable in the next section.

Deploying Spring Boot App to Heroku

Now its time to create an application in Heroku to deploy our Spring Boot App.

To get started, we have to sign-up for Heroku and get yourself a user account. Once you are registered, you can download Heroku CLI, where you can manage and deploy your applications on Heroku through the command line.

You can download and install the Heroku CLI at this link – https://devcenter.heroku.com/articles/heroku-cli#download-and-install

After you installed the Heroku CLI on your machine, type the below command and if you see the same output, then the CLI is installed correctly on your machine.

$ heroku --help
CLI to interact with Heroku

VERSION
  heroku/7.44.0 win32-x64 node-v12.16.2

USAGE
  $ heroku [COMMAND]

COMMANDS
  access          manage user access to apps
  addons          tools and services for developing, extending, and operating your app
  apps            manage apps on Heroku
  auth            check 2fa status
  authorizations  OAuth authorizations
  autocomplete    display autocomplete installation instructions
  buildpacks      scripts used to compile apps
  certs           a topic for the ssl plugin

Login to Heroku through CLI

As we are going to manage our applications through CLI, we are going to login to our account through Heroku CLI. You can login using the below commands:

$ heroku login -i
heroku: Enter your login credentials
Email: <your-email>
Password: <your-secret-password>
Logged in as <your-email>

Once you are successfully logged in, the next step is to create the application in Heroku. You can do that by typing the below command

$ heroku create
Creating app... done, ⬢ salty-reaches-63504
https://salty-reaches-63504.herokuapp.com/ | https://git.heroku.com/salty-reaches-63504.git

You can see that our application is created successfully, and it is reachable through a very strange looking URL. This is because Heroku uses the app name as part of the URL, so let’s go ahead and rename the app name to something we like.

$ heroku apps:rename --app salty-reaches-63504 spring-reddit-clone
Renaming salty-reaches-63504 to spring-reddit-clone... done
https://spring-reddit-clone.herokuapp.com/ | https://git.heroku.com/spring-reddit-clone.git
 !    Don't forget to update git remotes for all other local checkouts of the app.

If you try to open the newly created URL, you should see a Heroku Welcome page like below:

The last part of the configuration is to create a remote branch in git called as heroku, using this branch Heroku will automatically start the build and deployment of your application, once you pushed the source code to Git.

$ git remote add heroku https://git.heroku.com/spring-reddit-clone.git

$

Now its time to create a PostgreSQL Database instance. Heroku provides something called as addons where you can provision different services you need to run your application. Run the below command to provision a PostgreSQL Database addon

$ heroku addons:create heroku-postgresql --app spring-reddit-clone
Creating heroku-postgresql on ⬢ spring-reddit-clone... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-perpendicular-85041 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation

If you open the Heroku Cloud Console, under the Resources tab you can see the newly created PostgreSQL database.

By creating this addon, Heroku will detect we have a spring boot application and automatically configures for us the environment variables SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD which will override the properties spring.datasource.url, spring.datasource.username and spring.datasource.password properties in our application-prod.properties file.

Removing Credentials from source code

Storing any kind of username and passwords in source code is a bad practice, if you have any additional Credentials you are using in your application, you can inject them at run-time using the Heroku Config Variables.

In our application, we are using the spring.mail.username and spring.mail.password properties in our application.properties file, we can delete those values and use the below configuration variables

$ heroku config:set SPRING_MAIL_USERNAME=a7ca38e28c772f
Setting SPRING_MAIL_USERNAME and restarting ⬢ spring-reddit-clone... done, v10
SPRING_MAIL_USERNAME: a7ca38e28c772f

$ heroku config:set SPRING_MAIL_PASSWORD=8bf8e9295c7259
Setting SPRING_MAIL_PASSWORD and restarting ⬢ spring-reddit-clone... done, v11
SPRING_MAIL_PASSWORD: 8bf8e9295c7259

Now our application is ready to be deployed, run the below command by making sure you are inside the root directory of the spring boot application

$ git push heroku master
Enumerating objects: 560, done.
Counting objects: 100% (560/560), done.
Delta compression using up to 12 threads
Compressing objects: 100% (184/184), done.
Writing objects: 100% (560/560), 445.74 KiB | 44.57 MiB/s, done.
Total 560 (delta 284), reused 560 (delta 284)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Java app detected
remote: -----> Installing JDK 1.8... done
remote: -----> Executing Maven
remote:        $ ./mvnw -DskipTests clean dependency:list install

Once you type the above command you can see that Heroku automatically detected that we have a Java app and started running Maven Clean Install command.

You will see the below output once this step is completed.

remote:        [INFO] Installing /tmp/build_c9bd688c/target/spring-reddit-clone-0.0.1-SNAPSHOT.jar to /tmp/codon/tmp/cache/.m2/repository/com/programming/techie/spring-reddit-clone/0.0.1-SNAPSHOT/spring-reddit-clone-0.0.1-SNAPSHOT.jar
remote:        [INFO] Installing /tmp/build_c9bd688c/pom.xml to /tmp/codon/tmp/cache/.m2/repository/com/programming/techie/spring-reddit-clone/0.0.1-SNAPSHOT/spring-reddit-clone-0.0.1-SNAPSHOT.pom
remote:        [INFO] ------------------------------------------------------------------------
remote:        [INFO] BUILD SUCCESS
remote:        [INFO] ------------------------------------------------------------------------
remote:        [INFO] Total time:  24.013 s
remote:        [INFO] Finished at: 2020-10-05T21:14:37Z
remote:        [INFO] ------------------------------------------------------------------------
remote: -----> Discovering process types
remote:        Procfile declares types     -> (none)
remote:        Default types for buildpack -> web
remote:
remote: -----> Compressing...
remote:        Done: 95.2M
remote: -----> Launching...
remote:        Released v5
remote:        https://spring-reddit-clone.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/spring-reddit-clone.git
 * [new branch]      master -> master

Now if you open the link which is displayed in your console, in my instance it is https://spring-reddit-clone.herokuapp.com/ you will see the following page

Deploy Spring Boot Angular application Heroku

We are seeing a Whitelabel Error Page screen, that means our application is deployed successfully, let’s try to Register a user in our application. I am going to use POSTMAN to send a POST Request to the URL : https://spring-reddit-clone.herokuapp.com/api/auth/signup

Deploy Spring Boot Angular application Heroku

We got the response, we are expecting, which means the backend application is deployed successfully. Now its time to deploy the Angular application.

Preparing Angular app for Production

Now its time to make the Angular app production ready.

Maintain different URLs for Dev and Prod environments

We can make use of environments configuration in Angular, to inject different Backend Base URL based on the environment in which the app is running.

Add a new variable called baseUrl in environment.ts and environment.prod.ts files

environment.ts

export const environment = {
  production: false,
  baseUrl: 'http://localhost:8080/'
};

environment.prod.ts

export const environment = {
  production: true,
  baseUrl: 'https://spring-reddit-clone.herokuapp.com/'
};

If we use ng build and ng serve commands on your local setup, then the baseURL from environment.ts file will be injected.

If we are using ng build –prod command (see below section) then the baseUrl from the environment.prod.ts file will be injected.

Replace Hardcoded URL with environment variable

Now it’s time to replace the Hardcoded URL for our backend application with the environment variable baseUrl , you can find the service classes which are using the hardcoded URL below:

post.service.ts

import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {PostModel} from './post-model';
import {Observable} from 'rxjs';
import {CreatePostPayload} from '../post/create-post/create-post.payload';
import {environment} from "../../environments/environment";

@Injectable({
  providedIn: 'root'
})
export class PostService {

  baseUrl = environment.baseUrl;

  constructor(private http: HttpClient) {
  }

  getAllPosts(): Observable<Array<PostModel>> {
    return this.http.get<Array<PostModel>>(this.baseUrl + 'api/posts/');
  }

  createPost(postPayload: CreatePostPayload): Observable<any> {
    return this.http.post(this.baseUrl + 'api/posts/', postPayload);
  }

  getPost(id: number): Observable<PostModel> {
    return this.http.get<PostModel>(this.baseUrl + 'api/posts/' + id);
  }

  getAllPostsByUser(name: string): Observable<PostModel[]> {
    return this.http.get<PostModel[]>(this.baseUrl + 'api/posts/by-user/' + name);
  }
}

subreddit.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SubredditModel } from './subreddit-response';
import { Observable } from 'rxjs';
import {environment} from "../../environments/environment";

@Injectable({
  providedIn: 'root'
})
export class SubredditService {
  baseUrl = environment.baseUrl;
  constructor(private http: HttpClient) { }

  getAllSubreddits(): Observable<Array<SubredditModel>> {
    return this.http.get<Array<SubredditModel>>(this.baseUrl+'api/subreddit');
  }

  createSubreddit(subredditModel: SubredditModel): Observable<SubredditModel> {
    return this.http.post<SubredditModel>(this.baseUrl+'api/subreddit',
      subredditModel);
  }
}

vote.service.ts

import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {VotePayload} from './vote-button/vote-payload';
import {Observable} from 'rxjs';
import {environment} from "../../environments/environment";

@Injectable({
  providedIn: 'root'
})
export class VoteService {

  baseUrl = environment.baseUrl;

  constructor(private http: HttpClient) {
  }

  vote(votePayload: VotePayload): Observable<any> {
    return this.http.post(this.baseUrl + 'api/votes/', votePayload);
  }
}


auth.service.ts

import { Injectable, Output, EventEmitter } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SignupRequestPayload } from '../signup/singup-request.payload';
import { Observable, throwError } from 'rxjs';
import { LocalStorageService } from 'ngx-webstorage';
import { LoginRequestPayload } from '../login/login-request.payload';
import { LoginResponse } from '../login/login-response.payload';
import { map, tap } from 'rxjs/operators';
import {environment} from "../../../environments/environment";

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  @Output() loggedIn: EventEmitter<boolean> = new EventEmitter();
  @Output() username: EventEmitter<string> = new EventEmitter();
  baseUrl = environment.baseUrl;

  refreshTokenPayload = {
    refreshToken: this.getRefreshToken(),
    username: this.getUserName()
  }

  constructor(private httpClient: HttpClient,
    private localStorage: LocalStorageService) {
  }

  signup(signupRequestPayload: SignupRequestPayload): Observable<any> {
    return this.httpClient.post(this.baseUrl+'api/auth/signup', signupRequestPayload, { responseType: 'text' });
  }

  login(loginRequestPayload: LoginRequestPayload): Observable<boolean> {
    return this.httpClient.post<LoginResponse>(this.baseUrl+'api/auth/login',
      loginRequestPayload).pipe(map(data => {
        this.localStorage.store('authenticationToken', data.authenticationToken);
        this.localStorage.store('username', data.username);
        this.localStorage.store('refreshToken', data.refreshToken);
        this.localStorage.store('expiresAt', data.expiresAt);

        this.loggedIn.emit(true);
        this.username.emit(data.username);
        return true;
      }));
  }

  getJwtToken() {
    return this.localStorage.retrieve('authenticationToken');
  }

  refreshToken() {
    return this.httpClient.post<LoginResponse>(this.baseUrl+'api/auth/refresh/token',
      this.refreshTokenPayload)
      .pipe(tap(response => {
        this.localStorage.clear('authenticationToken');
        this.localStorage.clear('expiresAt');

        this.localStorage.store('authenticationToken',
          response.authenticationToken);
        this.localStorage.store('expiresAt', response.expiresAt);
      }));
  }

  logout() {
    this.httpClient.post(this.baseUrl+'api/auth/logout', this.refreshTokenPayload,
      { responseType: 'text' })
      .subscribe(data => {
        console.log(data);
      }, error => {
        throwError(error);
      })
    this.localStorage.clear('authenticationToken');
    this.localStorage.clear('username');
    this.localStorage.clear('refreshToken');
    this.localStorage.clear('expiresAt');
  }

  getUserName() {
    return this.localStorage.retrieve('username');
  }
  getRefreshToken() {
    return this.localStorage.retrieve('refreshToken');
  }

  isLoggedIn(): boolean {
    return this.getJwtToken() != null;
  }
}

comment.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { CommentPayload } from './comment.payload';
import { Observable } from 'rxjs';
import {environment} from "../../environments/environment";

@Injectable({
  providedIn: 'root'
})
export class CommentService {

  baseUrl = environment.baseUrl;

  constructor(private httpClient: HttpClient) { }

  getAllCommentsForPost(postId: number): Observable<CommentPayload[]> {
    return this.httpClient.get<CommentPayload[]>(this.baseUrl+'api/comments/by-post/' + postId);
  }

  postComment(commentPayload: CommentPayload): Observable<any> {
    return this.httpClient.post<any>(this.baseUrl+'api/comments/', commentPayload);
  }

  getAllCommentsByUser(name: string) {
    return this.httpClient.get<CommentPayload[]>(this.baseUrl+'api/comments/by-user/' + name);
  }
}

Add Heroku Post Build and Startup Step in package.json

Add the steps, heroku-postbuild and start to the scripts section of your package.json file

"scripts": {
    "ng": "ng",
    "heroku-postbuild": "ng build --prod && npm install -g http-server-spa",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "start": "http-server-spa dist/angular-reddit-clone index.html $PORT"
  },

The Heroku Post Build step will make sure to build the Angular App and install the HTTP Server to run our Angular application.

The Start step will make use of the HTTP Server which was installed in the previous step to start our application. Note that we have referenced the dist folder, which is generated from the Build step to http-server-spa

Deploying the Angular app to Heroku

As we did for Spring Boot application, let’s create the Angular app in Heroku using the below command:

$ heroku create
Creating app... done, ⬢ peaceful-headland-16641
https://peaceful-headland-16641.herokuapp.com/ | https://git.heroku.com/peaceful-headland-16641.git

# Rename the app
$ heroku apps:rename --app peaceful-headland-16641 spring-angular-reddit-clone
Renaming peaceful-headland-16641 to spring-angular-reddit-clone... done
https://spring-angular-reddit-clone.herokuapp.com/ | https://git.heroku.com/spring-angular-reddit-clone.git
Git remote heroku updated
 !    Don't forget to update git remotes for all other local checkouts of the app.

# Add Remote URL
$ git remote add heroku https://git.heroku.com/spring-angular-reddit-clone.git

# Push To Heroku
$ git push heroku master

The above list of commands should Create, Rename the Application in Heroku followed by uploading your source code and building it.

You should see the output like below after completing the Push to Heroku step

remote: -----> Pruning devDependencies
remote:        removed 1325 packages and audited 91 packages in 19.363s
remote:
remote:        5 packages are looking for funding
remote:          run `npm fund` for details
remote:
remote:        found 23 low severity vulnerabilities
remote:          run `npm audit fix` to fix them, or `npm audit` for details
remote:
remote: -----> Build succeeded!
remote: -----> Discovering process types
remote:        Procfile declares types     -> (none)
remote:        Default types for buildpack -> web
remote:
remote: -----> Compressing...
remote:        Done: 52.3M
remote: -----> Launching...
remote:        Released v3
remote:        https://spring-angular-reddit-clone.herokuapp.com/ deployed to Heroku

Now the Angular application is ready to be accessed at https://spring-angular-reddit-clone.herokuapp.com/

Updating Spring Boot CORS Policy

So we have our Spring Boot and Angular Application up and running, if you have followed in the same way you should have both the applications, communicating which each other, without any problems.

But we have to make one small change to the Spring Boot CORS Policy, in this example, we have allowed access to the Spring Boot REST API from all origins (hosts), ideally we should limit this access to only the client Origin locations, so in our case, we will allow access to the REST API only from our Angular Application.

WebConfig.java

package com.programming.techie.springredditclone.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {
        corsRegistry.addMapping("/**")
                .allowedOrigins("https://spring-angular-reddit-clone.herokuapp.com")
                .allowedMethods("*")
                .maxAge(3600L)
                .allowedHeaders("*")
                .exposedHeaders("Authorization")
                .allowCredentials(true);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");

        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

After making this changes push your changes to Heroku like below:

$ git add .

$ git commit -m "Changed allowed origins"
[master 437e7c3] Changed allowed origins
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git push heroku master
Enumerating objects: 21, done.
Counting objects: 100% (21/21), done.
Delta compression using up to 12 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (11/11), 759 bytes | 759.00 KiB/s, done.
Total 11 (delta 4), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
.....
.....
.....
.....
.....

remote:        [INFO] ------------------------------------------------------------------------
remote:        [INFO] BUILD SUCCESS
remote:        [INFO] ------------------------------------------------------------------------
remote:        [INFO] Total time:  13.631 s
remote:        [INFO] Finished at: 2020-10-10T19:38:11Z
remote:        [INFO] ------------------------------------------------------------------------
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 96.3M
remote: -----> Launching...
remote:        Released v19
remote:        https://spring-reddit-clone.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/spring-reddit-clone.git
   0ee9fde..437e7c3  master -> master

Conclusion

Alright, so we deployed our Reddit Clone application successfully to Heroku. I hope you learned something new from this tutorial.

I will see you in another article, until then Happy Learning 🙂

About the author 

Sai Upadhyayula

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

Subscribe now to get the latest updates!