Code Monkey home page Code Monkey logo

kotlin-http4k-realworld-example-app's Introduction

RealWorld Example App

Java CI codecov

Kotlin + http4k codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the RealWorld spec and API.

This codebase was created to demonstrate a fully fledged fullstack application built with Kotlin + http4k + Exposed including CRUD operations, authentication, routing, pagination, and more.

For more information on how to this works with other frontends/backends, head over to the RealWorld repo.

How it works

The application was made mainly to demo the functionality of http4k framework together with exposed.

Tech stack

The application was built with:

  • Kotlin as programming language.
  • http4k as web framework.
  • h2, an embedded lightweight database, as data storage. Although the application can support all the databases supported by exposed.
  • exposed to access database and build typesafe SQL queries.
  • jsonwebtoken to handle JSON Web Tokens for request authorization.
  • log4j for proper logging in the application.
  • kotest as testing framework for kotlin.
  • mockk as mocking library for Kotlin.

Application structure

Basically, the application has four main parts:

  • main function which instantiates all the services and handlers and connects them together, then starts the server.
  • Router class which handles the translation of 1. http requests to request handler calls and 2. handler results to http responses. Authorization logic is also implemented in this class.
  • handler package which contains a class for every request handler. Handlers are classes with one method (invoke) which handles the request. This package contains the whole business logic of the application. Handlers have access to data layer.
  • ConduitRepository class which is responsible for building database queries. In order to communicate to database, a class called ConduitTransactionManagerImpl is needed. this class has one method (tx) which connects to database and opens a transaction to communicate with database. tx accepts an anonymous function while provides an instance of ConduitRepository as a receiver.
+ config/
    Application config as simple data classes
+ handler/
    All handler classes which describe business logic of the application
+ model/
    domain model and dtos
+ repository/
    table definitions, typesafe database creation scripts and classes to communicate with database
+ util/
    utility classes such as request filters, serialization/deserialization functions and jwt utility functions
+ Main.kt
    File containing main function
+ Router.kt
    File containing Router class responsible for handling http server communication

Database

The application currently uses H2 embedded database. The connection is defined in config/local.kt. If you want to change the database you need to provide the correct dependency for the driver and change the configuration.

Tests

You can run ./gradlew test to run all the tests. A test logger gradle plugin (com.adarshr.test-logger) has used to render the test beautifully in console. There are a couple of unit tests for handlers but not for all of them (contributions welcome!). There are integration tests to cover all the cases of the postman test file.

Getting started

You need Java 11 installed.

Build and run tests:

./gradlew clean build

Start the server:

./gradlew run

The server will be available on http://localhost:9000

Contribution

There are various ways to contribute to this project. Some of them are:

  • Just clone the project and play with it! This is the purpose of this project.
  • Create an issue if you find a bug or you have suggestions.
  • Fix bugs, improve code or documentation.
  • Write more tests for the project to increase the code coverage.
  • Or look at the issue with help wanted label.

kotlin-http4k-realworld-example-app's People

Contributors

alisabzevari avatar alonski avatar azure-pipelines[bot] avatar daviddenton avatar ericsimons avatar esakkiraj avatar jeffcjohnson avatar optikfluffel avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

kotlin-http4k-realworld-example-app's Issues

Create article comment

Swagger description:

"post": {
"summary": "Create a comment for an article",
"description": "Create a comment for an article. Auth is required",
"tags": [
"Comments"
],
"security": [
{
"Token": []
}
],
"operationId": "CreateArticleComment",
"parameters": [
{
"name": "slug",
"in": "path",
"required": true,
"description": "Slug of the article that you want to create a comment for",
"type": "string"
},
{
"name": "comment",
"in": "body",
"required": true,
"description": "Comment you want to create",
"schema": {
"$ref": "#/definitions/NewCommentRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/SingleCommentResponse"
}
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
}
},

Delete article

Swagger description:

"delete": {
"summary": "Delete an article",
"description": "Delete an article. Auth is required",
"tags": [
"Articles"
],
"security": [
{
"Token": []
}
],
"operationId": "DeleteArticle",
"parameters": [
{
"name": "slug",
"in": "path",
"required": true,
"description": "Slug of the article to delete",
"type": "string"
}
],
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
}
},

Separate route handlers

There is only one class containing all the routes and their handlers. Having separated route handlers categorized by top-level endpoint name (e.g. tags, profiles etc) makes sense.

Delete article comment

Swagger description:

"/articles/{slug}/comments/{id}": {
"delete": {
"summary": "Delete a comment for an article",
"description": "Delete a comment for an article. Auth is required",
"tags": [
"Comments"
],
"security": [
{
"Token": []
}
],
"operationId": "DeleteArticleComment",
"parameters": [
{
"name": "slug",
"in": "path",
"required": true,
"description": "Slug of the article that you want to delete a comment for",
"type": "string"
},
{
"name": "id",
"in": "path",
"required": true,
"description": "ID of the comment you want to delete",
"type": "integer"
}
],
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
}
},

Get articles feed

Swagger description:

"/articles/feed": {
"get": {
"summary": "Get recent articles from users you follow",
"description": "Get most recent articles from users you follow. Use query parameters to limit. Auth is required",
"tags": [
"Articles"
],
"security": [
{
"Token": []
}
],
"operationId": "GetArticlesFeed",
"parameters": [
{
"name": "limit",
"in": "query",
"description": "Limit number of articles returned (default is 20)",
"required": false,
"default": 20,
"type": "integer"
},
{
"name": "offset",
"in": "query",
"description": "Offset/skip number of articles (default is 0)",
"required": false,
"default": 0,
"type": "integer"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/MultipleArticlesResponse"
}
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
}
},

Write unit test for CreateArticleFavoriteHandler

Suggested test cases:

  • Should throw proper exception when article not found.
  • If it is not a favorited article, it should call insertFavorite of repository.
  • Should throw proper exception when article doesn't have an author.
  • The favoritesCount in the return value object should has been increased by one.

Simplify the relation between handler and repository

Currently, handler classed does not contain the logic of the application. Instead, almost all the logic is implemented in ConduitRepository. I think after using exposed having a separate class to hold all queries don't make sense anymore. The better abstraction would be having a (mockable) class which holds the database connection and transaction function.

Unfollow user

Swagger docs:

"delete": {
"summary": "Unfollow a user",
"description": "Unfollow a user by username",
"tags": [
"Profile"
],
"security": [
{
"Token": []
}
],
"operationId": "UnfollowUserByUsername",
"parameters": [
{
"name": "username",
"in": "path",
"description": "Username of the profile you want to unfollow",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/ProfileResponse"
}
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
}
},

Write unit tests for CreateArticleHandler

Suggested test cases:

  • Article slug should properly being generated from title.
  • It should call insertArticle from repository.
  • It should return the created article with proper initial values.

Get article

Swagger description:

"/articles/{slug}": {
"get": {
"summary": "Get an article",
"description": "Get an article. Auth not required",
"tags": [
"Articles"
],
"operationId": "GetArticle",
"parameters": [
{
"name": "slug",
"in": "path",
"required": true,
"description": "Slug of the article to get",
"type": "string"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/SingleArticleResponse"
}
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
},

Create article

Swagger description:

"post": {
"summary": "Create an article",
"description": "Create an article. Auth is required",
"tags": [
"Articles"
],
"security": [
{
"Token": []
}
],
"operationId": "CreateArticle",
"parameters": [
{
"name": "article",
"in": "body",
"required": true,
"description": "Article to create",
"schema": {
"$ref": "#/definitions/NewArticleRequest"
}
}
],
"responses": {
"201": {
"description": "OK",
"schema": {
"$ref": "#/definitions/SingleArticleResponse"
}
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
}
},

Unable to see anything when running.

$ javac -version
javac 11.0.6

$ ./gradlew run
...
[INFO ] o.e.j.s.Server - Started @3084ms
[INFO ] main - Server started on port 9000

curl http://localhost:9000/
(nothing)

Used Chrome: No webpage was found for the web address: http://localhost:9000/
HTTP ERROR 404

Looked with netstat. A server is listening on 9000:
$ netstat -ant|grep 9000
TCP 0.0.0.0:9000 0.0.0.0:0 LISTENING InHost
TCP [::]:9000 [::]:0 LISTENING InHost
TCP [::1]:9000 [::1]:59033 TIME_WAIT InHost
TCP [::1]:9000 [::1]:59034 TIME_WAIT InHost

Delete article favorite

Swagger description:

"delete": {
"summary": "Unfavorite an article",
"description": "Unfavorite an article. Auth is required",
"tags": [
"Favorites"
],
"security": [
{
"Token": []
}
],
"operationId": "DeleteArticleFavorite",
"parameters": [
{
"name": "slug",
"in": "path",
"required": true,
"description": "Slug of the article that you want to unfavorite",
"type": "string"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/SingleArticleResponse"
}
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
}
},

Get article comments

Swagger description:

"/articles/{slug}/comments": {
"get": {
"summary": "Get comments for an article",
"description": "Get the comments for an article. Auth is optional",
"tags": [
"Comments"
],
"operationId": "GetArticleComments",
"parameters": [
{
"name": "slug",
"in": "path",
"required": true,
"description": "Slug of the article that you want to get comments for",
"type": "string"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/MultipleCommentsResponse"
}
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
},

JWT token generation should be configurable

Currently, the token generation logic hard-codes the signing key and expiration. But these values should be configurable and provided by AppConfig. Signing key should be provided as an environment variable.

integration test

http4k is so nicely designed and it helps to write integration tests much easier. One idea could be to remove some of the unit tests (or replace unit tests with some general non-related-to-business tests) and test the whole business of the application based on the postman test file using integration tests.

Write unit test for CreateArticleHandler

Suggested test cases:

  • Article slug should properly being generated from title.
  • It should call insertArticle from repository.
  • It should return the created article with proper initial values.

Write unit tests for DeleteArticleHandler

Suggested test cases:

  • It should throw exception if the article does not belong to the current user.
  • It should call deleteArticle from repository.
  • it should throw exception when the article doesn't exist.

Create article favorite

Swagger description:

"/articles/{slug}/favorite": {
"post": {
"summary": "Favorite an article",
"description": "Favorite an article. Auth is required",
"tags": [
"Favorites"
],
"security": [
{
"Token": []
}
],
"operationId": "CreateArticleFavorite",
"parameters": [
{
"name": "slug",
"in": "path",
"required": true,
"description": "Slug of the article that you want to favorite",
"type": "string"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/SingleArticleResponse"
}
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
},

Organize model classes

model related classes are scattered everywhere in the project. They should belong to the right package and right place.

Get tags

Swagger description:

"/tags": {
"get": {
"summary": "Get tags",
"description": "Get tags. Auth not required",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/TagsResponse"
}
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
}
}
},

Get articles

Swagger description:

"/articles": {
"get": {
"summary": "Get recent articles globally",
"description": "Get most recent articles globally. Use query parameters to filter results. Auth is optional",
"tags": [
"Articles"
],
"operationId": "GetArticles",
"parameters": [
{
"name": "tag",
"in": "query",
"description": "Filter by tag",
"required": false,
"type": "string"
},
{
"name": "author",
"in": "query",
"description": "Filter by author (username)",
"required": false,
"type": "string"
},
{
"name": "favorited",
"in": "query",
"description": "Filter by favorites of a user (username)",
"required": false,
"type": "string"
},
{
"name": "limit",
"in": "query",
"description": "Limit number of articles returned (default is 20)",
"required": false,
"default": 20,
"type": "integer"
},
{
"name": "offset",
"in": "query",
"description": "Offset/skip number of articles (default is 0)",
"required": false,
"default": 0,
"type": "integer"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/MultipleArticlesResponse"
}
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
},

Write unit tests for DeleteArticleFavoriteHandler

Suggested test cases:

  • It should throw proper exception when article does not exist.
  • If the user has not been favorited the article before, the handler should not throw exceptions.
  • It should throw exception when the article author not found.
  • It should return proper values for the return object.

Update article

Swagger description:

"put": {
"summary": "Update an article",
"description": "Update an article. Auth is required",
"tags": [
"Articles"
],
"security": [
{
"Token": []
}
],
"operationId": "UpdateArticle",
"parameters": [
{
"name": "slug",
"in": "path",
"required": true,
"description": "Slug of the article to update",
"type": "string"
},
{
"name": "article",
"in": "body",
"required": true,
"description": "Article to update",
"schema": {
"$ref": "#/definitions/UpdateArticleRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/SingleArticleResponse"
}
},
"401": {
"description": "Unauthorized"
},
"422": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/GenericErrorModel"
}
}
}
},

Time is a side effect

The application uses time in different parts of the code. For example, when creating article. In order to be able to properly test handlers Clock should be a dependency of the handler.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.