Skip to main content

7 posts tagged with "Postgresql"

View All Tags

Effortless REST API Development with Spring Data REST and PostgreSQL

· 5 min read
Huseyin BABAL
Software Developer

Introduction

In today’s fast-paced software development world, building efficient and flexible REST APIs is essential. Spring Data REST simplifies this process significantly by allowing developers to quickly expose repository-based data models as RESTful services with minimal configuration. In this article, we’ll explore how easy it is to create a REST API using Spring Data REST and PostgreSQL, allowing you to focus on your business logic instead of boilerplate code.

Why Spring Data REST?

Spring Data REST is built on top of Spring Data repositories and automatically exposes them as RESTful endpoints. Instead of manually writing controller logic to handle CRUD operations, Spring Data REST provides out-of-the-box support for:

  • Automatic generation of REST endpoints.
  • Hypermedia as the Engine of Application State (HATEOAS) support.
  • Pagination, sorting, and filtering.
  • Integration with Spring Data repositories for database interaction.

By leveraging Spring Data REST, you can rapidly create a fully functional REST API with minimal code, and focus on business rules, rather than implementing boilerplate REST operations.

Persistence Layer

In this article, we will be using PostgreSQL as our database. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.

tip

Create a free database in Rapidapp in seconds here

Step-by-Step Guide to Creating Project

Project Initialization and Dependencies

We will be using Spring Boot and PostgreSQL to build a todo application. You can initialize a spring boot project by using Spring Boot CLI. Once installed, you can use following command to initialize a project with required dependencies.

spring init \
--dependencies=web,data-jpa,data-rest,postgresql \
--type=maven-project \
--javaVersion=21 \
spring-data-rest-example

Line 2: web for implementing REST endpoints, data-jpa for database persistence, data-rest for automatically expose your data as fully qualified REST endpoints, and postgresql for PostgreSQL driver.

Line 3: --type=maven-project for creating a Maven project.

Line 4: --javaVersion=21 we will use Java 21 to compile and run project.

Now that we initialized the project, go to the folder spring-data-rest-example and open it with your favourite IDE.

Implementing Entity and Repository

We have only one entity here, Product, which will be used to store our products. Let's create a new entity called Product as follows.

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
class Product {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
private BigDecimal price;
}

In order to manage Product entity in database, we will use following repository interface.


interface ProductRepository extends CrudRepository<Product, Integer>{}

Application Configuration

This section contains application level configurations such as the application name, datasource, and jpa as shown below:

application.yaml
spring:
application:
name: spring-data-rest-example
datasource:
url: <connection-string-from-rapidapp|or your own managed postgres url>
username: <username>
password: <password>
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update

Line 5: Connection URL for the PostgreSQL database. You can obtain this from Rapidapp or your own managed PostgreSQL service. It should have a format like jdbc:postgresql://<host>:<port>/<database>?sslmode=require.

That's it! You have successfully created a Spring Data REST API with PostgreSQL as the database. One second, we don't have controllers,services, etc... Yes, Spring Data REST will take care of all these for you. It simply analyzes your entity with the help of @RepositoryRestResource annotation you put in repository interface above and exposes it as REST endpoints.

You can now run the application as follows;

mvn spring-boot:run

If everything goes well, you can verify app by visiting http://localhost:8080/products in your browser.

{
"_embedded" : {
"products" : [ ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/products"
},
"profile" : {
"href" : "http://localhost:8080/profile/products"
}
}
}

Rest Endpoints

Once the repository is created, Spring Data REST will automatically expose CRUD endpoints for the Product entity. These include:

  • GET /products: List all products.
  • GET /products/{id}: Retrieve a specific product.
  • POST /products: Create a new product.
  • PUT /products/{id}: Update an existing product.
  • DELETE /products/{id}: Delete a product.

There’s no need to write additional controller classes; the endpoints are provided automatically based on your repository definition.

Customizing the REST Behavior

Although Spring Data REST provides default behavior, you can customize the endpoints to suit your needs. For instance, you can modify the base path, limit the exposed operations, or add custom validation.

  • Customizing base paths: You can change the default endpoint path by adding the @RepositoryRestResource annotation to the repository:
@RepositoryRestResource(path = "product")
public interface ProductRepository extends JpaRepository<Product, Long> {
}

Now, the base URL for product-related endpoints will be /product instead of /products.

  • Exposing specific fields: You can expose or hide fields by using Spring Data REST’s projection feature:
@Projection(name = "customProduct", types = { Product.class })
interface CustomProduct {
String getTitle();
}

This will limit the fields exposed in the API response for products and show only title field.

Demo

Create Product

curl -XPOST -H "Content-Type: application/json" \
http://localhost:8080/products -d \
'{"title": "Laptop", "price": 1000}'

List Products

curl -XGET http://localhost:8080/products

Update Product

curl -XPUT -H "Content-Type: application/json" \
http://localhost:8080/products/1 -d \
'{"title": "Laptop", "price": 1200}'

Delete Product

curl -XDELETE http://localhost:8080/products/1

Get Product

curl -XGET http://localhost:8080/products/1

Conclusion

Spring Data REST, combined with PostgreSQL, provides a fast and simple way to create fully functional REST APIs without the need for writing repetitive boilerplate code. With automatic CRUD operations, pagination, and HATEOAS support, you can focus on the unique aspects of your application and leave the rest to Spring Data REST. By adopting this approach, you can significantly reduce development time and quickly deliver high-quality APIs.

tip

You can find the complete source code for this project on GitHub.

Efficient Database Migrations with .NET Core, Entity Framework, and PostgreSQL

· 5 min read
Huseyin BABAL
Software Developer

Introduction

In modern software development, managing database schema changes effectively is crucial. A smooth database migration process helps maintain consistency across environments (development, staging, production) and enables teams to iterate quickly. In this article, we’ll explore how to streamline database migrations in a .NET Core application using Entity Framework Core (EF Core) with PostgreSQL.

Why Migrations Matter?

Like Flyway in Java, EF Core migrations provide a way to evolve the database schema without losing data. It allows you to apply incremental changes to your schema while keeping track of the historical state of the database.

Persistence Layer

In this article, we will be using PostgreSQL as our database. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.

tip

Create a free database in Rapidapp in seconds here

Step-by-Step Implementation

We’ll be working with a simple .NET Core project using EF Core to interact with a PostgreSQL database. Below are the steps to get started.

Project Initialization

We will use .NET CLI to create a new .NET Core project. Let's start by creating a new project:

dotnet new webapi -n net-migration

Above command will initialize a new .NET Core Web API project named net-migration.

Installing Dependencies

We need to install the following packages:

  • Npgsql.EntityFrameworkCore.PostgreSQL: The PostgreSQL provider for Entity Framework Core.
  • Microsoft.EntityFrameworkCore.Design: The EF Core design package for migrations.
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design

Datasource Configuration

Configure PostgreSQL Connection in appsettings.json, add the PostgreSQL connection string:

{
"ConnectionStrings": {
"PostgresConnection": "Host=<host>;Port=<port>;Database=<database>;Username=<user>;Password=<password>;SSL Mode=require;Trust Server Certificate=true"
}
}

If you are using Rapidapp for PostgreSQL, you can find the connection string in the Rapidapp console and it supports SSL by default.

If you are using a local PostgreSQL instance, you can replace the placeholders with your PostgreSQL credentials and you may need to disable SSL if you haven't configured it in your database server.

Creating the Database Context

Create a ApplicationDbContext class that inherits from DbContext, configure it to use PostgreSQL.

ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;

namespace net_migration;

public class ApplicationDbContext : DbContext
{
public DbSet<Product> Products { get; set; }

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}

This is a generic database context configuration and the connection parameters will be passed in Program.cs soon.

Creating the Product Model

Create a Product class to represent the product entity in the database:

Product.cs
namespace net_migration;

public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}

Registering the Database Context

In the Program.cs file, register the ApplicationDbContext with the PostgreSQL connection string as follows.

Program.cs
...
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
...

Program.cs contains a couple of more boilerplate for the controllers, swagger, etc. However, to keep it simple, we are only showing the relevant part about registering the database context. The connection string is fetched from appsettings.json and passed to the UseNpgsql method.

Running Migrations

The basic skeleton of the project is ready. Now, let's create the database schema by running the migrations:

dotnet ef migrations add InitialCreate
dotnet ef database update

The InitialCreate migration will create the Products table in the database. The database update command will apply the migration to the database. If you haven't ef sub command for .NET CLI tool, you can install dotnet-ef extension as follows.

dotnet tool install --global dotnet-ef

The migration history is stored in the __EFMigrationsHistory table, which keeps track of the applied migrations. Assume that you added a new field called Description field to your Product entity. You can create a new migration and update the database as follows:

Product.cs
namespace net_migration;

public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }

// New column
public string Description { get; set; } // This is the new column you're adding.
}
dotnet ef migrations add AddDescriptionColumn
dotnet ef database update

You can see it will add a new column to Products table. You can also see the history in the __EFMigrationsHistory table.

Automate Migration on Startup

You can configure your application to automatically apply migrations at startup as follows.

using (var scope = app.ApplicationServices.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
dbContext.Database.Migrate();
}

Conclusion

Entity Framework Core makes database migrations straightforward in a .NET Core project. By managing schema changes through migrations, you ensure that your database remains in sync with your application's models across all environments. PostgreSQL, when combined with EF Core, provides a robust, scalable solution for modern web applications.

tip

You can find the complete source code for this project on GitHub.

Building Simple Product API with Apollo GraphQL and PostgreSQL

· 7 min read
Huseyin BABAL
Software Developer

Introduction

GraphQL has transformed how we build APIs by offering a flexible and powerful alternative to REST. It enables clients to request exactly the data they need, making it a great fit for modern applications. In this article, we'll explore GraphQL, its use cases, and key concepts like queries, mutations, and resolvers. We'll also walk you through building a simple product API using Apollo GraphQL with RapidApp PostgreSQL as your datasource.

What is GraphQL?

GraphQL is a query language for your API that allows clients to request specific data, avoiding over-fetching or under-fetching. Unlike REST, where multiple endpoints might be needed, GraphQL provides a single endpoint through which clients can access multiple data sources and types with one request.

Use Cases

GraphQL is widely used for:

  • Applications with complex data relationships
  • APIs that need to return different data to different clients
  • Projects aiming to optimize network usage
  • Microservices architecture integration

For example, you can use GraphQL to retrieve only the required fields from a product database, improving efficiency in e-commerce applications.

What Are Queries, Mutations, and Resolvers?

  • Query: Used to read or fetch data. In our case, we will create a query to retrieve products.
  • Mutation: Used to create, update, or delete data. We will create mutations to add new products to our datasource.
  • Resolvers: Functions that handle the execution of a query or mutation and interact with the datasource, such as a PostgreSQL database.

Persistence Layer

In this article, we will be using PostgreSQL as our database. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.

tip

Create a free database in Rapidapp in seconds here

Step-by-Step Implementation

Project Initialization

We will use Node.js and it already has a package manager called npm. Let's start by creating a new Node.js project:

mkdir pg-graphql
cd pg-graphql
npm init -y

Installing Dependencies

We need to install the following packages:

  • apollo-server: A GraphQL server library for Node.js.
  • graphql: The JavaScript reference implementation for GraphQL.
  • pg: A PostgreSQL client for Node.js.
  • datasource-sql: A SQL datasource for Apollo Server.
npm install apollo-server graphql pg datasource-sql

Creating Schema

Create a new file named schema.js in the root directory of your project and define the schema for your product API:

schema.js
const { gql } = require('apollo-server');

const typeDefs = gql`
type Product {
id: ID!
name: String!
price: Float!
description: String
}

type Query {
products: [Product]
product(id: ID!): Product
}

type Mutation {
createProduct(name: String!, price: Float!, description: String): Product
updateProduct(id: ID!, name: String, price: Float, description: String): Product
}
`;

module.exports = typeDefs;

Line 4: Defines the Product type with fields id, name, price, and description.

Line 11: Defines the Query type with two queries: products to fetch all products and product to fetch a single product by ID.

Line 16: Defines the Mutation type with two mutations: createProduct to add a new product and updateProduct to update an existing product.

This is kind of a contract of our API, and we will be using this in the server definition soon.

Implementing Product API

Create a new file named product_api.js in the root directory of your project and implement the product API:

product_api.js
const { SQLDataSource } = require('datasource-sql');

class ProductAPI extends SQLDataSource {
getProducts() {
return this.knex.select('*').from('products');
}

getProductById(id) {
return this.knex('products').where({ id }).first();
}

updateProduct(id, product) {
return this.knex('products')
.where({ id })
.update(product)
.returning('*')
.then(rows => rows[0]);
}

createProduct({ name, price, description }) {
return this.knex('products')
.insert({ name, price, description })
.returning('*');
}
}

module.exports = ProductAPI;

Knex is a SQL query builder for Node.js that we will use to interact with our PostgreSQL database.

Line 5: Retrieves all products from the products table.

Line 9: Retrieves a single product by ID.

Line 13: Updates an existing product by ID.

Line 21: Adds a new product to the products table.

Now that we have our API, let's use it in resolvers to handle queries and mutations.

Implementing Resolvers

Create a new file named resolvers.js in the root directory of your project and implement the resolvers:

resolvers.js
const resolvers = {
Query: {
products: async (_, __, { dataSources }) => {
return dataSources.productAPI.getProducts();
},
product: async (_, { id }, { dataSources }) => {
return dataSources.productAPI.getProductById(id);
},
},
Mutation: {
createProduct: async (_, { name, price, description }, { dataSources }) => {
return dataSources.productAPI.createProduct({ name, price, description });
},
updateProduct: async (_, { id, name, price, description }, { dataSources }) => {
const updatedProduct = { name, price, description };
return dataSources.productAPI.updateProduct(id, updatedProduct);
},
},
};

module.exports = resolvers;

Resolvers are functions that execute queries and mutations. They interact with the datasource to fetch or update data. In our case, it uses our Product API for better abstraction. With the help of resolvers, queries and mutations will be available in our GraphQL API.

Setting Up Apollo Server

Create a new file named server.js in the root directory of your project and set up the Apollo Server:

server.js
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
const ProductAPI = require('./product_api');

// PostgreSQL connection setup
const knexConfig = {
client: 'pg',
connection: {
host: '<host>',
user: '<user>',
password: '<password>',
database: '<database>',
ssl: {
rejectUnauthorized: false
},
application_name: 'apollo'
},
pool: { min: 0, max: 3 },
};

const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
productAPI: new ProductAPI(knexConfig),
}),
introspection: true,
});

server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

Line 22: ApolloServer is initialized with the schema, resolvers, and data sources. With the instrospection option set to true, you can explore the schema using tools like GraphQL Playground.

Running Application

Run the following command to start your Apollo Server:

node server.js

Testing the API

Once the server is ready, navigate to http://localhost:4000 in your browser to access the GraphQL Playground. You will see a button to go to the Apollo Studio page where you can explore your schema, run queries, and test mutations.

Creating a New Product

To create a new product, run the following mutation in the Playground:

mutation {
createProduct(name: "New Product", price: 99.99, description: "description") {
id
name
price
description
}
}

Fetching All Products

To fetch all products, run the following query:

query {
products {
id
name
price
description
}
}

Getting a Single Product

query {
product(id: 1) {
id
name
price
description
}
}

Updating a Product

mutation {
updateProduct(id: 1, name: "Updated Product", price: 199.99, description: "New Description") {
id
name
price
description
}
}

Conclusion

In this article, we covered how to set up a simple product API using Apollo GraphQL and PostgreSQL as a service. GraphQL’s flexibility with queries and mutations, combined with a powerful PostgreSQL datasource, makes it a great solution for building efficient APIs. If you're looking for a managed PostgreSQL solution, consider using RapidApp PostgreSQL to get started quickly.

tip

You can find the complete source code for this project on GitHub.

Simplifying DB Auditing with Spring Boot and Javers

· 8 min read
Huseyin BABAL
Software Developer

Introduction

Database auditing is essential for tracking changes to data over time, providing a clear history of who changed what and when. It helps ensure compliance with security and legal standards, allowing developers to detect anomalies, recover data, or conduct audits efficiently. In this article, we will explore how to implement automatic database auditing in a Spring Boot application using Javers, a powerful Java library that simplifies object auditing and diffing.

What is Javers?

Javers is a Java library designed for auditing object changes, often in databases. By comparing versions of objects, Javers creates an audit trail of differences between them. Unlike traditional database auditing methods that rely on triggers or manually logging changes, Javers operates at the application layer, making it easier to track object changes while maintaining flexibility.

Use Cases for Javers

  • Tracking Entity Changes: Javers can record changes to your domain objects and persist those changes, enabling easy retrieval of past object versions.

  • Data Synchronization: It can be used in distributed systems to ensure data consistency across multiple systems by comparing snapshots of the same object.

  • Data Governance and Compliance: For businesses that need to comply with regulations like GDPR, Javers provides an efficient way to track and monitor changes.

Understanding Javers' Tables

Javers uses a set of database tables to store the change history. These tables include:

  • jv_commit: Stores information about who made changes, the timestamp, and the commit ID.
  • jv_snapshot: Records object state at the time of a commit.
  • jv_global_id: Manages global identifiers for entities.
  • jv_commit_property: Stores metadata related to the commit, like environment details. These tables are automatically created when Javers is integrated with your Spring Boot project, allowing you to persist audit information without any manual setup.

In Javers, the type column is used to indicate the type of change captured in a snapshot. This column helps distinguish between various operations on an entity. Here's an explanation of the most common type values:

  • INITIAL: Indicates the first time an object is added to the database or tracked by Javers. This value represents the creation of the entity, meaning that there was no prior state to compare against.

  • UPDATE: Represents changes made to an existing object. When any field within the entity is modified, Javers captures the updated state and marks the change with this type. It indicates that this is not a new entity but an updated one.

  • TERMINAL: This value signifies that the entity has been deleted. When an object is removed from the system, Javers marks this event with a TERMINAL entry to signify the final state of the object before deletion.

  • INITIALIZED: This type appears in some cases to signify that a value was initialized or restored from a default state.

Persistence Layer

In this article, we will be using PostgreSQL as our database. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.

tip

Create a free database in Rapidapp in seconds here

Step-by-Step Implementation

We will implement a REST endpoint to create and update products to see db audit history by using Javers. It contains implementing entity, repository interface and a controller to create/update product.

Dependencies

Be sure you have the following dependencies installed by using your favourite dependency management tool e.g. maven, gradle.

pom.xml
 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<!-- This is for automatic db auiditing with the integration to spring security -->
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-spring-boot-starter-sql</artifactId>
<version>7.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Spring Web Security Configuration

In order to enable Spring Web Security and to be able to send request to REST endpoints, you need to configure it in your SecurityConfig.java file as shown below. By default, Spring expects to see csrf tokens in every request. With the following configuration, you can disable csrf protection.

SecurityConfig.java
@Configuration
@EnableWebSecurity
class WebSecurityConfiguration {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(withDefaults())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}

Line 2: Add @EnableWebSecurity to the SecurityConfig class to protect the API endpoints.

Line 7: Disable CSRF protection.

Line 12: Set the session creation policy to STATELESS to ensure sessions are not maintained.

Add the Product Entity

Create a simple Product entity that will be audited by Javers.

Product.java
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
class Product {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
}

Implement the Product Repository

Create a simple ProductRepository interface that will be audited by Javers.

ProductRepository.java
@JaversSpringDataAuditable
interface ProductRepository extends CrudRepository<Product, Long> {}

Line 1: Add @JaversSpringDataAuditable to the ProductRepository interface to enable Javers auditing for the product entity automatically.

Implement the Product Controller

Create a simple ProductController class for creating and updating products. We use POST method for creating, and PATCH method for updating single field of Product.

ProductController.java
@RestController
@RequestMapping("/products")
@RequiredArgsConstructor
class ProductController {

private final ProductRepository productRepository;

@PostMapping
public Product createProduct() {
Product product = new Product();
product.setName("Product " + System.currentTimeMillis());
return productRepository.save(product);
}

@PatchMapping
public void updateProduct() {
productRepository.findById(1L).ifPresent(product -> {
product.setName("Product " + System.currentTimeMillis());
productRepository.save(product);
});
}
}

Application Configuration

Next, configure database access in your application.yaml file. At a minimum, you need to specify the database connection details:

application.yaml
spring:
application:
name: spring-db-audit
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://<host>:<port>/<user>?sslmode=require&application_name=spring-db-audit&user=<user>&password=<password>
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
security:
user:
name: developer
password: s3cr3t

Line 13: Set the user property to developer and password to s3cr3t. We will use those credentials in curl requests to test the application.

Demo

Create Product

curl -XPOST \
-u developer:s3cr3t -H \
"Content-Type: application/json" \
http://localhost:8080/products -v

Javers will detect the following changes in the Product entity and save them in the database as follows.

> select *from jv_commit;
commit_pk | author | commit_date | commit_date_instant | commit_id
-----------+--------+-------------------------+-----------------------------+-----------
100 | user | 2024-09-08 01:58:49.728 | 2024-09-07T22:58:49.728964Z | 1.00
> select *from jv_snapshot;
snapshot_pk | type | version | state | changed_properties | managed_type | global_id_fk | commit_fk
-------------+---------+---------+------------------------------------+--------------------+------------------------------------+--------------+-----------
100 | INITIAL | 1 | { +| [ +| com.rapidapp.springdbaudit.Product | 100 | 100
| | | "name": "Product 1725749929156",+| "name", +| | |
| | | "id": 1 +| "id" +| | |
| | | } | ] | | |

Since this is a create operation, it is saved as INITIAL. Let's update product entity to see how Javers maintains its changes.

Update Product

curl -XPATCH \
-u developer:s3cr3t -H \
"Content-Type: application/json" \
http://localhost:8080/products -v

Javers will detect the following changes in the Product entity and save them in the database as follows.

> select *from jv_snapshot WHERE type='UPDATE';
snapshot_pk | type | version | state | changed_properties | managed_type | global_id_fk | commit_fk
-------------+--------+---------+------------------------------------+--------------------+------------------------------------+--------------+-----------
500 | UPDATE | 2 | { +| [ +| com.rapidapp.springdbaudit.Product | 100 | 400
| | | "name": "Product 1725777539405",+| "name" +| | |
| | | "id": 1 +| ] | | |
| | | } | | | |

As you can see;

  • type field is UPDATE
  • changed_properties is name to understand which field(s) are changed.
  • We can follow the history by checking version field. So, who did this change? Let's take a look at jv_commit table.
> select *from jv_commit order by commit_id desc limit 1;
commit_pk | author | commit_date | commit_date_instant | commit_id
-----------+--------+-------------------------+-----------------------------+-----------
400 | john | 2024-09-08 09:38:59.639 | 2024-09-08T06:38:59.639471Z | 5.00

You can see the author field to see the owner of this change. As you can see, john is the owner of this change.

Conclusion

By using Javers with Spring Boot, database auditing becomes simple and flexible. Javers' ability to track object changes over time and store audit data in a structured way makes it a great choice for any application that requires a detailed history of data changes. With this implementation, you can now easily integrate automatic auditing in your Spring Boot applications.

tip

You can find the complete source code for this project on GitHub.

Building an Application with JHipster, PostgreSQL, and Elasticsearch in 10 Minutes

· 7 min read
Huseyin BABAL
Software Developer

Introduction

In today’s fast-paced development landscape, creating robust and scalable applications quickly is essential. Leveraging jHipster, PostgreSQL, and Elasticsearch can streamline this process. This article walks you through the steps of building a demo project, showcasing the integration of these powerful tools in just 10 minutes.

Why jHipster?

jHipster accelerates application development by providing a complete stack, including front-end and back-end technologies. It generates high-quality code, follows best practices, and offers extensive tooling, making it a go-to solution for developers seeking efficiency and reliability.

Prerequisites

PostgreSQL

In this article, we will be using PostgreSQL as our database. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.

tip

Create a free database in Rapidapp in seconds here

Elasticsearch

We will be using Elasticsearch for the search engine. Elasticsearch is an open-source, distributed, and scalable search engine which you can deploy on-premises or in the cloud. You can use Elastic Cloud if you don't want to maintain your own instance.

jHipster CLi

To get started with jHipster, you'll need to install jHipster CLi.

Getting Started

You can simply run jhipster command in your terminal and follow the prompts to get started as shown below. Do not forget to provide your own namings to fields like application name, package name etc.

? What is the base name of your application? demo
? Which *type* of application would you like to create? Monolithic application (recommended for simple projects)
? What is your default Java package name? com.huseyinbabal.demo
? Would you like to use Maven or Gradle for building the backend? Maven
? Do you want to make it reactive with Spring WebFlux? No
? Which *type* of authentication would you like to use? JWT authentication (stateless, with a token)
? Besides JUnit, which testing frameworks would you like to use?
? Which *type* of database would you like to use? SQL (H2, PostgreSQL, MySQL, MariaDB, Oracle, MSSQL)
? Which *production* database would you like to use? PostgreSQL
? Which *development* database would you like to use? PostgreSQL
? Which cache do you want to use? (Spring cache abstraction) Ehcache (local cache, for a single node)
? Do you want to use Hibernate 2nd level cache? Yes
? Which other technologies would you like to use? Elasticsearch as search engine
? Which *framework* would you like to use for the client? React
? Besides Jest/Vitest, which testing frameworks would you like to use?
? Do you want to generate the admin UI? Yes
? Would you like to use a Bootswatch theme (https://bootswatch.com/)? Default JHipster
? Would you like to enable internationalization support? No
? Please choose the native language of the application English

This will generate a full-stack application where PostgreSQL and Elasticsearch is configured and enabled during application application startup. Now that you have the basic setup, you can start configuring the datasource.

PostgreSQL Configuration

Once you open project folder in your favourite IDE, you can see the generated application*.yaml files under src/main/resources/config folder. Since we are doing local development for now, you can open application-dev.yaml and configure datasource as follows.

application-dev.yaml
spring:
datasource:
url: jdbc:postgresql://<host>:<port>/<db_name>?sslmode=require&application_name=rapidapp_jhipster # You can find details on Rapidapp db details page.
username: <username>
password: <password>

Line 3: You can find the DB connection details on Rapidapp db details page.

Elasticsearch Configuration

We will configure Elasticsearch as a search engine in our application. Once you create your own Elasticsearch instance, or create one in Elastic Cloud, note your Elasticsearch credentials to use them in the following configuration section.

application-dev.yaml
spring:
elasticsearch:
uris: https://elastic:<password>@<host>:<port>

Running the Application

Now that you have configured your database and search engine, you can start the application with the following command:

./mvn

Above command will do the followings;

  • Build the frontend and backend projects
  • Start the backend project while running liquibase asynchronously. Liquibase will prepare the database schema by using your entities.
  • Start the frontend project. If everything goes well, you will see an output as follows;
2024-06-19T17:01:11.056+03:00  INFO 65684 --- [  restartedMain] com.huseyinbabal.jdemo.JDemoApp          :
----------------------------------------------------------
Application 'jDemo' is running! Access URLs:
Local: http://localhost:8080/
External: http://192.168.1.150:8080/
Profile(s): [dev, api-docs]
----------------------------------------------------------

You can simply navigate to http://localhost:8080/ to access the application. It will show you the default credentials for users with admin and user rights, you can login with admin:admin credentials to see how admin UI looks like. You can see the critical components below;

  • Entities: Entities used in this application. We will see this soon to create our own entities to use in the application.
  • Administration > Metrics: You can see several metrics like JVM, Cache, HTTP statistics.
  • Administration > Health: You can see the health information of the application like db, disk health.
  • Administration > Logs: You can see the log configuration of the application where you can set log level in root or package level. Feel free to walk through the menus in Admin UI menu to get familiar with them, meanwhile, let's see how we can add our own entities to application.

Adding Entities

We will add our own entities to the application. Let's create a new entity called Product and add it to the application with the following command

jhipster entity product

It will prompt you to add fields for this entity. You can use following fields;

  • title: String
  • description: String
  • price: Float Once it is done, it will create necessary entity in codebase and related controller for the CRUD operations. src/main/java/<package>/domain/Product.java contains the generated entity class and src/main/java/<package>/repository/ProductRepository.java contains the generated repository class. In order to access resource information which is the presentation layer, you can take a look at src/main/java/<package>/web/rest/ProductResource.java. Let's take a look at how it creates a product as shown below.
ProductResource.java
@PostMapping("")
public ResponseEntity<Product> createProduct(@RequestBody Product product) throws URISyntaxException {
log.debug("REST request to save Product : {}", product);
if (product.getId() != null) {
throw new BadRequestAlertException("A new product cannot already have an ID", ENTITY_NAME, "idexists");
}
product = productRepository.save(product);
productSearchRepository.index(product);
return ResponseEntity.created(new URI("/api/products/" + product.getId()))
.headers(HeaderUtil.createEntityCreationAlert(applicationName, false, ENTITY_NAME, product.getId().toString()))
.body(product);
}

Line 8: productSearchRepository.index(product) is used to index the product in Elasticsearch. You see how easy it is to store product data in elasticsearch. We haven't written any code for that, but since we have added elasticsearch config, jHipster becomes an elasticsearch-aware system where it also generates required functions for you.

In same way, let's see how it searches product as shown below.

ProductResource.java
@GetMapping("/_search")
public ResponseEntity<List<Product>> searchProducts(
@RequestParam("query") String query,
@org.springdoc.core.annotations.ParameterObject Pageable pageable
) {
log.debug("REST request to search for a page of Products for query {}", query);
try {
Page<Product> page = productSearchRepository.search(query, pageable);
HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page);
return ResponseEntity.ok().headers(headers).body(page.getContent());
} catch (RuntimeException e) {
throw ElasticsearchExceptionMapper.mapException(e);
}
}

Line 8: productSearchRepository.search(query, pageable) is used to search product in elasticsearch.

Product Entity in Admin UI

As you can see, we have created a product entity in Admin UI. You can see it in Entities menu. Once you navigate to Product module, you can create new Product, list products, view details of a product or delete any of them. Also, there is a search bar where you can search products with the help of Elasticsearch on the backend side.

Conclusion

We have seen that you can leverage jHipster's rapid development capabilities along with the robust data management of PostgreSQL and powerful search functionality of Elasticsearch. jHipster simplifies and accelerates application creation with its comprehensive toolset and best practices, while PostgreSQL ensures reliable and efficient data handling. Elasticsearch adds advanced search capabilities, making your application both scalable and responsive. Utilizing Rapidapp's PostgreSQL as a service further streamlines database management, allowing you to focus on developing high-quality applications quickly and effectively.

tip

You can find the complete source code for this project on GitHub.

Securing Your Spring Boot App with JWT Authentication

· 8 min read
Huseyin BABAL
Software Developer

Introduction

This article dives into securing a Spring Boot application using JSON Web Tokens (JWT) for authentication. We'll explore Spring Security, JWT fundamentals, and then implement a secure API with user registration, login, and access control. Our data will be persisted in a PostgreSQL database using Spring Data JPA.

Why Spring Security?

Spring Security is an industry-standard framework for securing Spring applications. It offers comprehensive features for authentication, authorization, and access control. By leveraging Spring Security, we can efficiently manage user access to our API endpoints.

JWT Authentication Explained

JWT is a token-based authentication mechanism. Unlike traditional session-based methods, JWT stores user information in a compact, self-contained token. This token is sent with every request, allowing the server to verify the user's identity without relying on server-side sessions.

Here's a breakdown of JWT's benefits:

  • Stateless: Removes the need for session management on the server.
  • Secure: Employs digital signatures to prevent tampering.
  • Flexible: Can be configured with various claims to store user information.

Persistence Layer

In this article, we will be using PostgreSQL as our database. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.

tip

Create a free database in Rapidapp in seconds here

Step-by-Step Implementation

Dependencies

Be sure you have the following dependencies installed by using your favourite dependency management tool e.g. maven, gradle.

pom.xml
 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Enabling Spring Web Security

In order to enable Spring Web Security, you need to configure it in your SecurityConfig.java file as shown below.

SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private static final String[] AUTH_WHITELIST = {
"/api/v1/auth/login",
"/api/v1/auth/register"
};

private final JwtAuthFilter jwtAuthFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeRequests(authorizeRequests ->
authorizeRequests
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sessionManagement ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

Line 2: Add @EnableWebSecurity to the SecurityConfig class to protect the API endpoints.

Line 6: Allow requests from the /api/v1/auth/login and /api/v1/auth/register endpoints without authentication.

Line 16: Disable CSRF protection, since JWT authentication is stateless.

Line 24: Set the session creation policy to STATELESS to ensure sessions are not maintained.

Line 25: Add the JwtAuthFilter to the security filter chain before the UsernamePasswordAuthenticationFilter. We will explain JwtAuthFilter class soon.

JWT Auth Filter

In order to enable JWT authentication, you need to configure it in your JwtAuthFilter.java file as shown below.

JwtAuthFilter.java
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getServletPath().contains("/api/v1/auth")) {
filterChain.doFilter(request, response);
return;
}

final String authorizationHeader = request.getHeader("Authorization");
final String jwtToken;
final String email;

if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

jwtToken = authorizationHeader.substring(7);
email = jwtService.extractEmail(jwtToken);

if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
if (jwtService.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response);
}
}

Line 10: Do not apply JWT auth filter for /api/v1/auth endpoints.

Line 24: Extract JWT token from the Authorization header. Its format is Bearer <token>, that's why it is substring(7).

Line 25: Extract email from the JWT token using JwtService which we will take a look at in the next section.

Line 28-32: Validate the JWT token using JwtService, load user details using UserDetails from UserDetailsService and store the authentication in SecurityContextHolder.

Implementing JWTService

This class contains all JWT related functionalities as shown below.

JwtService.java
@Service
public class JwtService {

@Value("${jwt.secret}")
private String secret;

public String extractEmail(String jwtToken) {
return extractClaim(jwtToken, Claims::getSubject);
}

public <T> T extractClaim(String jwtToken, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(jwtToken);
return claimsResolver.apply(claims);
}

private Claims extractAllClaims(String jwtToken) {
return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(jwtToken).getPayload();
}

private SecretKey getSigningKey() {
byte [] bytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(bytes);
}

public boolean validateToken(String jwtToken, UserDetails userDetails) {
final String email = extractEmail(jwtToken);
return email.equals(userDetails.getUsername()) && !isTokenExpired(jwtToken);
}

private boolean isTokenExpired(String jwtToken) {
return extractExpiration(jwtToken).before(new Date());
}

private Date extractExpiration(String jwtToken) {
return extractClaim(jwtToken, Claims::getExpiration);
}

public String generateToken(User u) {
return createToken(u.getEmail());
}

private String createToken(String email) {
return Jwts.builder()
.subject(email)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(getSigningKey())
.compact();
}
}

Line 5: This is the secret key used to sign JWT tokens. This should be carefully protected, it is not something that we can share or expose publicly. All the other functions are self-explanatory.

UserDetailsService

UserDetailsService is design for showing spring boot security authentication how to load user details from database as shown below.

UserDetailsService.java
@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {
private final UserRepository userRepository;


@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.map(user -> User.builder().username(user.getEmail())
.password(user.getPassword())
.build())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}

Until this point, we have only focused on JWT authentication. However, how we will generate JWT tokens in the next section? What is its use-case?

Registering User

Before generating JWT token to authenticate the user, we need to register the user. We will use AuthController to register user.

AuthController.java
@RestController
@RequestMapping(path = "api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;


@PostMapping(path = "/register")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void register(@RequestBody RegisterRequest registerRequest) {
authService.register(registerRequest);
}

@PostMapping(path = "/login")
public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
return ResponseEntity.ok(authService.login(loginRequest));
}
}

In above controller, we are using AuthService to register and login user. AuthService uses UserRepository to interact database for user related operations.

AuthService.java
@Service
@RequiredArgsConstructor
public class AuthService {

private final UserRepository userRepository;
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;

public void register(RegisterRequest registerRequest) {
User u = User.builder()
.email(registerRequest.getEmail())
.password(bCryptPasswordEncoder.encode(registerRequest.getPassword()))
.firstName(registerRequest.getFirstName())
.lastName(registerRequest.getLastName())
.build();
userRepository.save(u);
}

public String login(LoginRequest loginRequest) {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
User u = userRepository.findByEmail(loginRequest.getEmail()).orElseThrow(() -> new EntityNotFoundException("User not found"));
return jwtService.generateToken(u);

}
}

Line 10: Register user by using the details provided in the request payload. The bCryptPasswordEncoder is used to hash the password before storing it in the database.

Line 21: The login operation is done through authenticationManager since it knows how to validate username and password.

Restricted Access to UserController

You can see a sample endpoint implementation for user object.

UserController.java
@RestController
@RequestMapping(path = "api/v1")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}

@GetMapping("/users")
public List<User> getUsers() {
return userRepository.findAll();
}
}

Assume you registered a new user with email admin password ssshhhh. Then in order to generate a JWT token, you can use the following curl request.

curl -X POST -H "Content-Type: application/json" \
-d '{"email": "admin", "password": "ssshhhh"}' http://localhost:8080/api/v1/auth/login

It will return a JWT token, which you can use to authenticate the user. Store it somewhere.

Now in order to access restricted user endpoint, you can use the following curl request.

curl -X GET -H "Authorization: Bearer <token>" http://localhost:8080/api/v1/users

Conclusion

This hands-on tutorial equipped you with the knowledge to implement JWT Authentication in your Spring Boot application. We explored user registration, login, and access control, leveraging Spring Security and JPA for data persistence. By following these steps and customizing the code examples to your specific needs, you can secure your API endpoints and ensure authorized user access. Remember to prioritize security best practices. Here are some additional points to consider:

  • Secret Key Management: Store your JWT secret key securely in environment variables or a dedicated secret management service. Never expose it in your codebase.
  • Token Expiration: Set a reasonable expiration time for JWT tokens to prevent unauthorized access due to compromised tokens.
  • Error Handling: Implement proper error handling mechanisms for invalid or expired tokens to provide informative feedback to users.
  • Advanced Features: Explore advanced JWT features like refresh tokens for longer-lived sessions and role-based access control (RBAC) for granular authorization. With JWT authentication in place, your Spring Boot application is well on its way to becoming a secure and robust platform. Deploy it with confidence, knowing that user access is properly controlled.
tip

You can find the complete source code for this project on GitHub.

Building a Realtime Chat App with React, Node.js, and PostgreSQL

· 6 min read
Huseyin BABAL
Software Developer

Introduction

In today's interactive world, real-time communication thrives. This article guides you through building a basic real-time chat application using React for the frontend, Node.js for the backend, and PostgreSQL for data persistence. We'll leverage WebSockets to establish a persistent connection between clients (web browsers) and the server, enabling instant message updates.

Understanding WebSockets

Imagine a two-way highway where messages flow seamlessly between clients and servers. That's the essence of WebSockets. Unlike traditional HTTP requests, which are one-off interactions, WebSockets facilitate a long-lasting connection, allowing for real-time data exchange.

WebSockets in Action

WebSockets empower a variety of applications:

  • Chat Applications: Deliver messages instantly, fostering a more engaging conversation experience.
  • Collaborative Editing: Enable multiple users to work on a document simultaneously, seeing changes in real-time.
  • Stock Tickers: Continuously update stock prices on a financial dashboard.
  • Multiplayer Games: Facilitate smooth real-time interactions among players.
  • Social Media Updates: Receive notifications and updates without page reloads.

PostgreSQL: A Robust Database

PostgreSQL, a powerful open-source object-relational database (ORM), provides an excellent platform for storing chat messages and potentially user information. Its key features include:

  • ACID Transactions: Ensure data integrity through atomicity, consistency, isolation, and durability.
  • Scalability: Handle large chat datasets efficiently.
  • Flexibility: Model complex data structures for additional features. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.
tip

Create a free database in Rapidapp in seconds here

Backend: Node.js WebSocket Server

Here's a breakdown of the Node.js code utilizing websocket to establish a WebSocket connection and handle message broadcasting:

server.js
const WebSocket = require('websocket').server;
const http = require('http');
const cors = require('cors');
const Sequelize = require('sequelize');

const sequelize = new Sequelize(process.env.DATABASE_URL)

const Message = sequelize.define('Message', {
username: Sequelize.DataTypes.STRING,
text: Sequelize.DataTypes.STRING,
timestamp: Sequelize.DataTypes.DATE
});

sequelize.authenticate().then(() =>{
console.log("Connection has been established successfully.")
Message.sync();
createServer();
}).catch((err) => {
console.error("Unable to connect to the database:", err)
})

function createServer() {

const httpServer = http.Server((req, res) => {
cors()(req, res, () => {
if (req.url === '/messages') {
fetchMessages().then((messages) => {
res.writeHead(200, {
'Content-Type': 'application/json'
})
res.end(JSON.stringify(messages))
})
}
})
})

const webSocketServer = new WebSocket({
httpServer: httpServer
})

webSocketServer.on('request', (req) => {
const connection = req.accept(null, req.origin);

connection.on('message', (message) => {
let msg = JSON.parse(message.utf8Data)
Message.create({
username: msg.username,
text: msg.text,
timestamp: msg.timestamp

})
webSocketServer.broadcast(JSON.stringify(message))
})

connection.on('close', () => {
// defer conn
})
})

httpServer.listen(3005, () => console.log('Listening on port 3005'));
}

function fetchMessages() {
return Message.findAll();
}

Line 1-4: Import necessary modules and libraries. We use the websocket library to create a WebSocket server. The http module is used to create an HTTP server, and cors is used to enable Cross-Origin Resource Sharing to be able to access http backend from a different origin.

Line 6: Sequelize is a Node.js ORM library to access various databases. Create a Sequelize instance with the connection string from Rapidapp or your own managed PostgreSQL service. The url format should be like postgresql://<user>:<password>@<host>:<port>/<dbname>?ssl=true&sslmode=no-verify&application_name=<client_name>

Line 8-12: Define a Message model with username, text, and timestamp fields.

Line 14-20: Authenticate the connection to the database and create the Message table if it doesn't exist. If the connection is successful, start the server.

Line 24-35: Create an HTTP server to handle incoming requests. If the request URL is /messages, fetch all messages from the database and return them as JSON.

Line 37-39: Create a WebSocket server using the websocket library and attach it to the HTTP server.

Line 44-53: Handle incoming WebSocket requests. When a message is received, parse it, save it to the database, and broadcast it to all connected clients.

Line 60: Start the HTTP server on port 3005.

Frontend: React with WebSocket Connection

Here's the React component managing the chat interface, message sending, and receiving messages through the WebSocket connection:

App.js
import './App.css';
import {useEffect, useState, useRef} from "react";

function App() {

const [messages, setMessages] = useState([]);
const [messageInput, setMessageInput] = useState('');

const socket = useRef(null);

const [username, setUsername] = useState('');

const [existingUserName, setExistingUsername] = useState(localStorage.getItem('username') || '');


useEffect(() => {
socket.current = new WebSocket('ws://localhost:3005');

socket.current.onopen = () => {
console.log('Connected successfully.');
}

socket.current.onmessage = (event) => {
const msg = JSON.parse(event.data);
setMessages([...messages, JSON.parse(msg['utf8Data'])])
}

return () => {
socket.current.close();
};
}, [messages]);

useEffect(() => {
fetchMessages();
}, []);


const sendMessage = () => {
if (messageInput.trim() === '') {
return
}

const message = {
text: messageInput,
username: existingUserName,
timestamp: new Date().toISOString(),
}

socket.current.send(JSON.stringify(message))
setMessageInput('')
}

const fetchMessages = () => {
fetch('http://localhost:3005/messages')
.then((res) => res.json())
.then((data) => {
setMessages(data)
})
}

return (
<div className="App">
{existingUserName ? (
<div className="chat-container">
<div className="chat-messages">
{messages.map((message, index) => (
<div className="message sent">
<span className="message-timestamp">{message['username']}</span>
<span className="message-content">{message['text']}</span>
</div>
))}
</div>
<div className="chat-input">
<input
type="text"
placeholder="Type your message"
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && messageInput.trim() !== '') {
sendMessage();
}
}}
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>) : (
<div className="chat-input">
<input
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<button onClick={() => {
const u=username.trim()
localStorage.setItem('username', u);
setExistingUsername(u)
}}>Connect</button>
</div>
)}
</div>
);
}

export default App;

Line 17: Connect to the WebSocket server running on ws://localhost:3005.

Line 23: Handle incoming messages from the WebSocket server. Parse the message and update the messages list.

Line 34: Fetch messages from the server when the component mounts.

Line 38: A function that sends a message to the WebSocket server.

Line 63: Render the chat interface. If the user has not set a username, prompt them to enter one.

Conclusion

This article has provided a foundational understanding of building a real-time chat application using React, Node.js, WebSockets, and PostgreSQL. The code snippets demonstrate the core functionalities of establishing a WebSocket connection, sending messages, broadcasting them to connected clients, and persisting messages in a database. Remember:

  • This is a simplified example, and real-world applications might require additional features like user authentication, authorization, message editing/deletion, and handling disconnects gracefully.
  • Consider implementing security measures to protect your application from malicious attacks.
  • For large-scale applications, further optimize the database structure and explore scaling strategies.
  • By building upon this foundation and tailoring it to your specific needs, you can create a dynamic and engaging real-time chat application.
tip

You can find the complete source code for this project on GitHub.