A RESTful forum API built with Hapi.js and PostgreSQL, implementing Clean Architecture. Supports user registration, JWT-based authentication, threads, and comments with soft-delete. Built as a Dicoding Back-End Expert submission.
- Runtime: Node.js 18
- Framework: Hapi.js v20
- Database: PostgreSQL 14 via
pg - Auth: JWT (HS256) with
hapi-auth-jwt2 - DI Container:
instances-container - Migrations:
node-pg-migrate - CI/CD: GitHub Actions — tests on PRs, deploy to AWS EC2 (PM2) on push to
master
- Node.js 18+
- PostgreSQL running locally
- Two databases: one for development, one for tests
npm installCreate a .env file in the project root (see .env.example pattern):
HOST=localhost
PORT=5000
PGHOST=localhost
PGUSER=your_user
PGDATABASE=forumapi
PGPASSWORD=your_password
PGPORT=5432
PGHOST_TEST=localhost
PGUSER_TEST=your_user
PGDATABASE_TEST=forumapi_test
PGPASSWORD_TEST=your_password
PGPORT_TEST=5432
ACCESS_TOKEN_KEY=<64-byte hex string>
REFRESH_TOKEN_KEY=<64-byte hex string>
ACCCESS_TOKEN_AGE=3000Run migrations:
npm run migrate up # dev database
npm run migrate:test up # test databaseStart the server:
npm run start:dev # nodemon (development)
npm start # productionTests hit a real PostgreSQL database — no mocks. NODE_ENV=test tells the pool to use PG*_TEST credentials.
# run full test suite
NODE_ENV=test npm test
# watch mode with coverage report
NODE_ENV=test npm run test:watch
# run a single file
NODE_ENV=test npx jest --setupFiles dotenv/config -i path/to/file.test.js
# run a single test by name
NODE_ENV=test npx jest --setupFiles dotenv/config -i -t "test description"All responses follow { status, data } or { status } envelopes. Protected routes require Authorization: Bearer <accessToken>.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/users |
— | Register a new user |
POST /users body: { username, password, fullname }
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/authentications |
— | Login — returns accessToken + refreshToken |
PUT |
/authentications |
— | Refresh access token |
DELETE |
/authentications |
— | Logout (invalidate refresh token) |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/threads |
Required | Create a thread |
GET |
/threads/{thread_id} |
— | Get thread detail with comments |
POST /threads body: { title, body }
GET /threads/{thread_id} returns the thread with its full comment list. Deleted comments show content as **komentar telah dihapus**.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/threads/{thread_id}/comments |
Required | Add a comment |
DELETE |
/threads/{thread_id}/comments/{comment_id} |
Required | Delete own comment (soft-delete) |
POST .../comments body: { content }
Four-layer Clean Architecture — dependency direction points inward only.
Interfaces/http/api/ → HTTP plugins (Hapi routes + handlers)
Applications/use_case/ → Business logic orchestration
Domains/ → Abstract repositories + value entities
Infrastructures/ → Postgres repos, JWT, bcrypt, DI container
Key conventions:
Infrastructures/container.jsis the composition root — every new repository or use case must be registered there.- Entity constructors self-validate and throw uppercase error codes (e.g.
NEW_THREAD.NOT_CONTAIN_NEEDED_PROPERTY).DomainErrorTranslatormaps these to Indonesian-language HTTP errors. - IDs are
nanoid()-prefixed by type:thread-*,comment-*,user-*. - Threads and comments use soft-delete (
is_deletedcolumn).DELETE /commentsflips the flag; GET responses apply the masking in theExistingCommententity.
CI runs tests on every PR to master. Merging to master triggers SSH deployment to an EC2 instance:
git reset --hard origin/master → npm i → pm2 restart threads-api
Required GitHub secrets: SSH_PRIVATE_KEY, SSH_HOST, USER_NAME, ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, ACCESS_TOKEN_AGE.