I built RESTFUL api for a blog using java and spring boot(without a frontend).
What it does
the code manages all the http methods on a post inside the blog and comments on that post.
the post consists of an id, name of the creator, title, body, amount of likes and dislikes, the comments are very similar they consist of an id, id of the post it was left on, username of the comment poster, its body, likes and dislikes.
I have a mysql database for the posts and comments which columns are the same as the model.
for the posts and comments there is a controller that manages all the HTTP methods with a repository object that extends CrudRepository.
this is basically it I have a .http file to run the HTTP methods but it can be achieved through other means and services.
Post and Comment Model
package dev.ellie.blogcontentmanagmentsystem.models;
import org.springframework.data.annotation.Id;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record Post(
@Id
Integer id,
@NotBlank(message = "name should not be blank")
String creatorName,
@NotBlank(message = "post name should not be blank")
String postName,
@NotBlank(message = "content should not be blank")
@Size(min=20,max=1000, message = "content should be between 20 to 1000 characters long")
String content,
int likes,
int dislikes
) {
}
package dev.ellie.blogcontentmanagmentsystem.models;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Table(name="comment", schema = "blog-content-manager")
public record Comment(
@Id
@Column("id")
int id,
@Column("postId")
int postId,
@NotBlank(message = "username must not be blank")
@Column("commentUsername")
String commentUsername,
@Column("content")
@NotBlank(message = "content must not be blank")
@Size(min = 10, max = 200, message = "content must contain between 10 and 200 characters")
String content,
@Column("likes")
int likes,
@Column("dislikes")
int dislikes
) {
}
Post and Comment Controllers
package dev.ellie.blogcontentmanagmentsystem.controllers;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import dev.ellie.blogcontentmanagmentsystem.models.Post;
import dev.ellie.blogcontentmanagmentsystem.repository.AccessingPostDataMySql;
@RestController
@RequestMapping("/api/posts")
public class PostController {
//private final PostRepository repository;
private final AccessingPostDataMySql repository;
@Autowired
public PostController(AccessingPostDataMySql repository) {
this.repository = repository;
}
@GetMapping
public List<Post> findAll() {
return (List<Post>) repository.findAll();
}
@GetMapping("/{id}")
public Iterable<Post> findById(@PathVariable Iterable<Integer> id) {
return repository.findAllById(id);
}
@PostMapping
public void insertPost(@RequestBody Post post) {
System.out.println(post.postName());
repository.save(post);
}
@PutMapping("{id}/likes")
public int addLike(@PathVariable Integer id) throws Exception {
repository.addLike(id);
return 1;
}
@PutMapping("{id}/dislikes")
public int addDislike(@PathVariable Integer id) throws Exception {
repository.addDislike(id);
return 1;
}
}
package dev.ellie.blogcontentmanagmentsystem.controllers;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import dev.ellie.blogcontentmanagmentsystem.models.Comment;
import dev.ellie.blogcontentmanagmentsystem.repository.AccessingCommentDataMySql;
@RestController
@RequestMapping("/api/comments")
public class CommentController {
private final AccessingCommentDataMySql repository;
@Autowired
public CommentController(AccessingCommentDataMySql repository) {
this.repository = repository;
}
@GetMapping("{postId}")
public List<Comment> findAllByPostId(@PathVariable int postId) {
return (List<Comment>) repository.findAllByPostId(postId);
}
@GetMapping()
public List<Comment> findAll() {
return (List<Comment>) repository.findAll();
}
@PutMapping("{postId}/{id}/likes")
public int addLike(@PathVariable Integer postId,@PathVariable Integer id) {
repository.addLike(id);
return 1;
}
@PutMapping("{postId}/{id}/dislikes")
public int addDislike(@PathVariable Integer postId,@PathVariable Integer id) {
repository.addDislike(id);
return 1;
}
}
The Repository Interface for the Posts and Comments
package dev.ellie.blogcontentmanagmentsystem.repository;
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import dev.ellie.blogcontentmanagmentsystem.models.Post;
public interface AccessingPostDataMySql extends CrudRepository<Post, Integer> {
@Modifying
@Query("UPDATE post p SET likes = likes + 1 WHERE p.id = :id")
void addLike(@Param("id") Integer id);
@Modifying
@Query("UPDATE post p SET dislikes = dislikes + 1 WHERE p.id = :id")
void addDislike(@Param("id") Integer id);
}
package dev.ellie.blogcontentmanagmentsystem.repository;
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import dev.ellie.blogcontentmanagmentsystem.models.Comment;
public interface AccessingCommentDataMySql extends CrudRepository<Comment, Integer> {
@Query("SELECT * FROM comment c WHERE c.postId = :postId")
Iterable<Comment> findAllByPostId(@Param("postId") int postId);
@Modifying
@Query("UPDATE comment c SET likes = likes + 1 WHERE c.id = :id")
void addLike(@Param("id") Integer id);
@Modifying
@Query("UPDATE comment c SET dislikes = dislikes + 1 WHERE c.id = :id")
void addDislike(@Param("id") Integer id);
}
MySQL Schema
CREATE TABLE IF NOT EXISTS post (
id INT PRIMARY KEY,
creator_name VARCHAR(255),
post_name VARCHAR(255),
content VARCHAR(1000),
likes INT,
dislikes INT
);
CREATE TABLE IF NOT EXISTS comment (
id INT PRIMARY KEY,
postId INT,
commentUsername VARCHAR(255),
content VARCHAR(200),
likes INT,
dislikes INT
);
I would like some ideas on how to improve my code and what to add to it, thanks.
-
\$\begingroup\$ you can Isolate the AccessingPostDataMySql from the EndPoint. Creating a Service that contains the AccessingPostDataMySql and manipulate the business logic in the Service. \$\endgroup\$Omar Mahamid– Omar Mahamid2023年09月06日 14:28:48 +00:00Commented Sep 6, 2023 at 14:28
1 Answer 1
relation between post and comment
in my opinion the relation between comments post is a composition and therefor it should belong to the post.
@RestController
@RequestMapping("/api/posts/{postId}/comments")
public class CommentController {
...
@GetMapping
public List<Comment> findAllByPostId(@PathVariable int postId) {
return (List<Comment>) repository.findAllByPostId(postId);
}
...
}
path variables can be set at class level, there is an stackoverflow article describing how
this is what the API would look like after:
GET /api/posts/{postId}/comments - get all
GET /api/posts/{postId}/comments/{commentId}
PUT /api/posts/{postId}/comments/{commentId}/likes
PUT /api/posts/{postId}/comments/{commentId}/dislikes
missing POST for comments
you cannot create commonets with your API, there is no POST
to create new ones (as well as there is no PUT
nor DELETE
).
dangerous API
while it is described in any tutorial to provide a getAll()
method, it is dangerous in real applications. When data reaches a certain size, the amount of results will overburden the system.
BUT: it is quite common to use Pagination for that use case.
weird return values
why do you return the magic number 1 in your like/dislike methods? you can use void
methods.
using entities as dtos
there is a nice article from Martin Fowler addressing this issue, you should not send db-entities as dtos.
using a logger
just give it a try, you already try to log something... so do it now in a proper way, dont be afraid :-)
@PostMapping
public void insertPost(@RequestBody Post post) {
//System.out.println(post.postName());
//use a logger instead
repository.save(post);
}
summary
your code is very readable and very good to understand! i appreciate what you have achieved! Especially i like how you do dependency injection in the PROPER way! i do not see this very often, nice.
-
1\$\begingroup\$ thanks for the response and the summary (you made my day), I took a break from java for now but thanks for the tips, im currently working with C++ and im sure I can use your advice on my current project as it is somewhat simillar. \$\endgroup\$Ellie– Ellie2023年09月20日 14:42:24 +00:00Commented Sep 20, 2023 at 14:42
-
1\$\begingroup\$ for logging I suggest use @Slf4j from lombok \$\endgroup\$shareef– shareef2023年09月20日 17:57:23 +00:00Commented Sep 20, 2023 at 17:57
-
\$\begingroup\$ @Guy honestly it was really fun (?pleasure) to read your code, it is clearly written and has very little flaws - i for myself could not do better ^^ \$\endgroup\$Martin Frank– Martin Frank2023年09月22日 05:19:25 +00:00Commented Sep 22, 2023 at 5:19
-
\$\begingroup\$ @MartinFrank thank you very much for the kind words and im sure you can do better than me, im just a begginer :) \$\endgroup\$Ellie– Ellie2023年09月23日 16:11:17 +00:00Commented Sep 23, 2023 at 16:11