โ Back to Connections
Connect Spring Boot to PostPenguin
Medium SetupLanguage: Java / KotlinTime: 15 minutes
Spring Boot provides a robust framework for building Java applications. This guide shows you how to create a webhook endpoint that receives PostPenguin posts and stores them in your database.
๐ Quick Setup
1. Add Dependencies
<!-- pom.xml -->
<dependencies>
<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>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>2. Create Post Entity
// src/main/java/com/example/model/Post.java
package com.example.model;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.Type;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(name = "postpenguin_id", unique = true, nullable = false)
private String postpenguinId;
@Column(nullable = false, length = 500)
private String title;
@Column(unique = true, nullable = false)
private String slug;
@Column(columnDefinition = "TEXT", nullable = false)
private String html;
@Column(name = "meta_title", length = 500)
private String metaTitle;
@Column(name = "meta_description", columnDefinition = "TEXT")
private String metaDescription;
@Column(name = "featured_image", length = 1000)
private String featuredImage;
@Column(columnDefinition = "jsonb")
private String tags;
@Column(length = 50)
private String status = "publish";
@Column(name = "published_at")
private LocalDateTime publishedAt;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}3. Create Repository
// src/main/java/com/example/repository/PostRepository.java
package com.example.repository;
import com.example.model.Post;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface PostRepository extends JpaRepository<Post, String> {
Optional<Post> findByPostpenguinId(String postpenguinId);
Optional<Post> findBySlugAndStatus(String slug, String status);
Page<Post> findByStatusOrderByPublishedAtDesc(String status, Pageable pageable);
}4. Create DTOs
// src/main/java/com/example/dto/PostPenguinPayload.java
package com.example.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class PostPenguinPayload {
@JsonProperty("postPenguinId")
private String postPenguinId;
private String title;
private String slug;
private String html;
@JsonProperty("meta_title")
private String metaTitle;
@JsonProperty("meta_description")
private String metaDescription;
@JsonProperty("featured_image")
private String featuredImage;
private List<String> tags;
}
// src/main/java/com/example/dto/WebhookResponse.java
package com.example.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class WebhookResponse {
private boolean success;
private String postId;
private String postpenguinId;
private String action;
}5. Create Webhook Controller
// src/main/java/com/example/controller/WebhookController.java
package com.example.controller;
import com.example.dto.PostPenguinPayload;
import com.example.dto.WebhookResponse;
import com.example.model.Post;
import com.example.repository.PostRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
@Slf4j
@RestController
@RequestMapping("/api/webhooks")
@RequiredArgsConstructor
public class WebhookController {
private final PostRepository postRepository;
private final ObjectMapper objectMapper;
@Value("${postpenguin.webhook.secret:}")
private String webhookSecret;
@PostMapping("/postpenguin")
public ResponseEntity<?> handleWebhook(
@RequestBody String body,
@RequestHeader(value = "X-PostPenguin-Signature", required = false) String signature
) {
try {
// Verify signature
if (webhookSecret != null && !webhookSecret.isEmpty() && signature != null) {
if (!verifySignature(body, signature, webhookSecret)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Invalid signature"));
}
}
PostPenguinPayload payload = objectMapper.readValue(body, PostPenguinPayload.class);
// Validate required fields
if (payload.getTitle() == null || payload.getSlug() == null || payload.getHtml() == null) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Missing required fields"));
}
String postpenguinId = payload.getPostPenguinId() != null
? payload.getPostPenguinId()
: "pp_" + System.currentTimeMillis();
// Check if post exists
Optional<Post> existingPost = postRepository.findByPostpenguinId(postpenguinId);
Post post;
String action;
if (existingPost.isPresent()) {
post = existingPost.get();
action = "updated";
} else {
post = new Post();
post.setPostpenguinId(postpenguinId);
action = "created";
}
post.setTitle(payload.getTitle());
post.setSlug(payload.getSlug());
post.setHtml(payload.getHtml());
post.setMetaTitle(payload.getMetaTitle() != null ? payload.getMetaTitle() : payload.getTitle());
post.setMetaDescription(payload.getMetaDescription() != null ? payload.getMetaDescription() : "");
post.setFeaturedImage(payload.getFeaturedImage());
post.setTags(payload.getTags() != null ? objectMapper.writeValueAsString(payload.getTags()) : "[]");
post.setStatus("publish");
post.setPublishedAt(LocalDateTime.now());
post = postRepository.save(post);
log.info("Post {}: {}", action, payload.getTitle());
return ResponseEntity.ok(new WebhookResponse(true, post.getId(), postpenguinId, action));
} catch (Exception e) {
log.error("Webhook error", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Internal server error"));
}
}
private boolean verifySignature(String payload, String signature, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
String expected = hexString.toString();
String received = signature.replace("sha256=", "");
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
received.getBytes(StandardCharsets.UTF_8)
);
} catch (Exception e) {
log.error("Signature verification error", e);
return false;
}
}
}6. Create Posts API Controller
// src/main/java/com/example/controller/PostController.java
package com.example.controller;
import com.example.model.Post;
import com.example.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
public class PostController {
private final PostRepository postRepository;
@GetMapping
public ResponseEntity<?> getPosts(
@RequestParam(defaultValue = "10") int limit,
@RequestParam(defaultValue = "0") int offset
) {
Page<Post> posts = postRepository.findByStatusOrderByPublishedAtDesc(
"publish",
PageRequest.of(offset / limit, limit)
);
return ResponseEntity.ok(Map.of(
"posts", posts.getContent(),
"pagination", Map.of(
"total", posts.getTotalElements(),
"limit", limit,
"offset", offset
)
));
}
@GetMapping("/{slug}")
public ResponseEntity<?> getPostBySlug(@PathVariable String slug) {
return postRepository.findBySlugAndStatus(slug, "publish")
.map(post -> ResponseEntity.ok(Map.of("post", post)))
.orElse(ResponseEntity.notFound().build());
}
}โ๏ธ Configuration
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/postpenguin
username: your_username
password: your_password
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
postpenguin:
webhook:
secret: your-secret-key-here๐งช Testing
# Start the application
./mvnw spring-boot:run
# Test webhook
curl -X POST http://localhost:8080/api/webhooks/postpenguin \
-H "Content-Type: application/json" \
-d '{
"postPenguinId": "test_spring_123",
"title": "Test Spring Boot Post",
"slug": "test-spring-boot-post",
"html": "<p className="text-gray-700">This is a test post in Spring Boot.</p>",
"meta_title": "Test Post",
"meta_description": "Testing Spring Boot webhook",
"tags": ["spring", "java"]
}'
# Fetch posts
curl http://localhost:8080/api/posts
# Get single post
curl http://localhost:8080/api/posts/test-spring-boot-postNeed Help?
Check our webhook documentation for technical details, or contact support for custom integrations.