Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 251 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,285 @@ This codebase was created to demonstrate a fully fledged full-stack application

For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.

# *NEW* GraphQL Support
## Features

Following some DDD principles. REST or GraphQL is just a kind of adapter. And the domain layer will be consistent all the time. So this repository implement GraphQL and REST at the same time.
This application provides a complete backend implementation for a Medium-like blogging platform with the following features:

The GraphQL schema is https://github.com/gothinkster/spring-boot-realworld-example-app/blob/master/src/main/resources/schema/schema.graphqls and the visualization looks like below.
- User registration and authentication with JWT tokens
- User profiles with follow/unfollow functionality
- Article CRUD operations with slug-based URLs
- Article tagging system
- Article favorites
- Comments on articles
- Feed of articles from followed users
- Both REST API and GraphQL endpoints

![](graphql-schema.png)

And this implementation is using [dgs-framework](https://github.com/Netflix/dgs-framework) which is a quite new java graphql server framework.
# How it works
## GraphQL Support

The application uses Spring Boot (Web, Mybatis).
Following some DDD principles, REST or GraphQL is just a kind of adapter. The domain layer remains consistent regardless of the API type. This repository implements both GraphQL and REST simultaneously.

* Use the idea of Domain Driven Design to separate the business term and infrastructure term.
* Use MyBatis to implement the [Data Mapper](https://martinfowler.com/eaaCatalog/dataMapper.html) pattern for persistence.
* Use [CQRS](https://martinfowler.com/bliki/CQRS.html) pattern to separate the read model and write model.
The GraphQL schema is located at `src/main/resources/schema/schema.graphqls` and the visualization looks like below.

And the code is organized as this:
![](graphql-schema.png)

1. `api` is the web layer implemented by Spring MVC
2. `core` is the business model including entities and services
3. `application` is the high-level services for querying the data transfer objects
4. `infrastructure` contains all the implementation classes as the technique details
This implementation uses [dgs-framework](https://github.com/Netflix/dgs-framework), Netflix's GraphQL server framework for Spring Boot.

# Security
### How it works

Integration with Spring Security and add other filter for jwt token process.
The application uses Spring Boot (Web, Mybatis).

The secret key is stored in `application.properties`.
- Uses Domain Driven Design (DDD) to separate business logic from infrastructure concerns
- Uses MyBatis to implement the [Data Mapper](https://martinfowler.com/eaaCatalog/dataMapper.html) pattern for persistence
- Uses [CQRS](https://martinfowler.com/bliki/CQRS.html) pattern to separate read and write models
- Uses Lombok to reduce boilerplate code

## Project Structure

```
src/main/java/io/spring/
├── api/ # REST API layer (Spring MVC controllers)
│ ├── exception/ # Custom exceptions and error handling
│ └── security/ # JWT filter and security configuration
├── application/ # Application services layer
│ ├── article/ # Article command services and validation
│ ├── data/ # Data Transfer Objects (DTOs)
│ └── user/ # User command services and validation
├── core/ # Domain layer (entities and repository interfaces)
│ ├── article/ # Article entity and repository
│ ├── comment/ # Comment entity and repository
│ ├── favorite/ # ArticleFavorite entity and repository
│ ├── service/ # Domain service interfaces (JwtService, AuthorizationService)
│ └── user/ # User entity, FollowRelation, and repository
├── graphql/ # GraphQL layer (Netflix DGS datafetchers and mutations)
│ └── exception/ # GraphQL-specific exceptions
└── infrastructure/ # Infrastructure layer (implementations)
├── mybatis/ # MyBatis mappers, type handlers, and read services
├── repository/ # Repository implementations
└── service/ # Service implementations (DefaultJwtService)
```

## Requirements

- Java 11 or higher
- Gradle (wrapper included)

## Dependencies

The main dependencies used in this project:

| Dependency | Purpose |
|------------|---------|
| Spring Boot 2.6.3 | Application framework |
| Spring Security | Authentication and authorization |
| MyBatis | ORM/Data Mapper |
| Netflix DGS | GraphQL server framework |
| JJWT | JWT token generation and validation |
| Joda-Time | Date/time handling |
| SQLite | Database (default) |
| Lombok | Boilerplate code reduction |
| Flyway | Database migrations |

## Security

Integration with Spring Security with JWT token-based authentication.

The JWT configuration is stored in `application.properties`:
- `jwt.secret`: Secret key for signing tokens (HS512 algorithm)
- `jwt.sessionTime`: Token expiration time in seconds (default: 86400 = 24 hours)

## Database

Uses SQLite database by default for easy local testing without losing data after restart. Can be changed in `application.properties` for any other database.

## Getting started

# Database

It uses a ~~H2 in-memory database~~ sqlite database (for easy local test without losing test data after every restart), can be changed easily in the `application.properties` for any other database.
You'll need Java 11 installed.

# Getting started
```bash
./gradlew bootRun
```

You'll need Java 11 installed.
To test that it works, open a browser tab at http://localhost:8080/tags or run:

./gradlew bootRun
```bash
curl http://localhost:8080/tags
```

To test that it works, open a browser tab at http://localhost:8080/tags .
Alternatively, you can run
## Configuration

curl http://localhost:8080/tags
The main configuration file is `src/main/resources/application.properties`:

# Try it out with [Docker](https://www.docker.com/)
```properties
# Database
spring.datasource.url=jdbc:sqlite:dev.db
spring.datasource.driver-class-name=org.sqlite.JDBC

# JWT
jwt.secret=your-secret-key
jwt.sessionTime=86400

# MyBatis
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.mapper-locations=mapper/*.xml
```

## API Examples

### User Registration

```bash
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{
"user": {
"username": "johndoe",
"email": "john@example.com",
"password": "password123"
}
}'
```

### User Login

```bash
curl -X POST http://localhost:8080/users/login \
-H "Content-Type: application/json" \
-d '{
"user": {
"email": "john@example.com",
"password": "password123"
}
}'
```

### Get Current User

```bash
curl http://localhost:8080/user \
-H "Authorization: Token <your-jwt-token>"
```

### Create Article

```bash
curl -X POST http://localhost:8080/articles \
-H "Content-Type: application/json" \
-H "Authorization: Token <your-jwt-token>" \
-d '{
"article": {
"title": "How to train your dragon",
"description": "Ever wonder how?",
"body": "You have to believe",
"tagList": ["dragons", "training"]
}
}'
```

### Get Articles

```bash
# Get all articles
curl http://localhost:8080/articles

# Filter by tag
curl http://localhost:8080/articles?tag=dragons

# Filter by author
curl http://localhost:8080/articles?author=johndoe

# Filter by favorited
curl http://localhost:8080/articles?favorited=johndoe

# Pagination
curl http://localhost:8080/articles?limit=10&offset=0
```

### Get User Feed

```bash
curl http://localhost:8080/articles/feed \
-H "Authorization: Token <your-jwt-token>"
```

### Favorite Article

```bash
curl -X POST http://localhost:8080/articles/how-to-train-your-dragon/favorite \
-H "Authorization: Token <your-jwt-token>"
```

### Follow User

```bash
curl -X POST http://localhost:8080/profiles/johndoe/follow \
-H "Authorization: Token <your-jwt-token>"
```

### Add Comment

```bash
curl -X POST http://localhost:8080/articles/how-to-train-your-dragon/comments \
-H "Content-Type: application/json" \
-H "Authorization: Token <your-jwt-token>" \
-d '{
"comment": {
"body": "Great article!"
}
}'
```

## GraphQL API

The GraphQL endpoint is available at `http://localhost:8080/graphql`.

Example query:

```graphql
query {
articles(first: 10) {
edges {
node {
slug
title
description
author {
username
}
}
}
}
}
```

## Try it out with Docker

You'll need Docker installed.

./gradlew bootBuildImage --imageName spring-boot-realworld-example-app
docker run -p 8081:8080 spring-boot-realworld-example-app

# Try it out with a RealWorld frontend
```bash
./gradlew bootBuildImage --imageName spring-boot-realworld-example-app
docker run -p 8081:8080 spring-boot-realworld-example-app
```

## Try it out with a RealWorld frontend

The entry point address of the backend API is at http://localhost:8080, **not** http://localhost:8080/api as some of the frontend documentation suggests.

# Run test
## Run tests

The repository contains a lot of test cases to cover both api test and repository test.
The repository contains comprehensive test cases covering both API tests and repository tests.

./gradlew test
```bash
./gradlew test
```

# Code format
## Code format

Use spotless for code format.
Use Spotless for code formatting with Google Java Format.

./gradlew spotlessJavaApply
```bash
./gradlew spotlessJavaApply
```

# Help
## Contributing

Please fork and PR to improve the project.
46 changes: 46 additions & 0 deletions src/main/java/io/spring/JacksonCustomizations.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,72 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Configuration class for customizing Jackson JSON serialization.
*
* <p>This class provides custom Jackson modules and serializers to handle
* specific data types used in the RealWorld application, particularly
* Joda-Time DateTime objects.
*
* @see RealWorldModules
* @see DateTimeSerializer
*/
@Configuration
public class JacksonCustomizations {

/**
* Creates and registers the custom Jackson module for the RealWorld application.
*
* @return a Jackson module containing custom serializers
*/
@Bean
public Module realWorldModules() {
return new RealWorldModules();
}

/**
* Custom Jackson module that registers serializers for RealWorld-specific types.
*
* <p>This module registers a custom serializer for Joda-Time DateTime objects
* to ensure consistent date/time formatting across the API.
*/
public static class RealWorldModules extends SimpleModule {
/**
* Creates a new RealWorldModules instance and registers custom serializers.
*/
public RealWorldModules() {
addSerializer(DateTime.class, new DateTimeSerializer());
}
}

/**
* Custom Jackson serializer for Joda-Time DateTime objects.
*
* <p>This serializer converts DateTime objects to ISO 8601 formatted strings
* in UTC timezone, ensuring consistent date/time representation in JSON responses.
*
* <p>Example output: "2023-01-15T10:30:00.000Z"
*/
public static class DateTimeSerializer extends StdSerializer<DateTime> {

/**
* Creates a new DateTimeSerializer instance.
*/
protected DateTimeSerializer() {
super(DateTime.class);
}

/**
* Serializes a DateTime object to JSON.
*
* <p>The DateTime is formatted as an ISO 8601 string in UTC timezone.
* Null values are serialized as JSON null.
*
* @param value the DateTime value to serialize
* @param gen the JSON generator
* @param provider the serializer provider
* @throws IOException if an I/O error occurs during serialization
*/
@Override
public void serialize(DateTime value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
Expand Down
Loading