R.RAFI BAKHTIAR
SOFTWARE ENGINEER
OPEN TO WORK CV ↗
← BACK TO PROJECTS

PROJECT 02 · BACKEND REST API

Task
API.

task-api / routesGO · GIN · GORM

01 POST /api/auth/register

02 POST /api/auth/login

03 GET /api/me

04 GET /api/tasks

05 POST /api/tasks

RESPONSE · JSON (EMPTY LIST)
{
  "data": [],
  "meta": { "page": 1, "limit": 10, "total": 0 }
}
PROJECT TYPEBackend REST API
ROLEBackend developer
STACKGo · Gin · GORM · SQLite

01 / SCOPE

A small API, built with structure.

Every user has a private task list. The project is about the parts around plain CRUD: authentication, access limited to the owner, input validation, pagination, and documentation that another developer can run.

02 / WHAT IS INCLUDED

Auth, tasks,
and documentation.

01

Authentication

Register and log in. Passwords are hashed with bcrypt, and login returns a JWT that expires after 24 hours.

02

Task management

Create, read, update, and delete your own tasks, with paginated lists and a done filter.

03

Layered code

Routes in main.go, with handlers, JWT middleware, models, and token utilities in separate packages.

04

Documentation

An OpenAPI spec with Swagger UI at /docs, a Postman collection, and a multi-stage Dockerfile.

API ENDPOINTS

The API at a glance.

METHODPATHACCESSNOTE
GET /health Public Server status check.
POST /api/auth/register Public Create an account. The password is hashed with bcrypt; a duplicate email returns 409.
POST /api/auth/login Public Returns a JWT that is valid for 24 hours.
GET /api/me Login required Profile of the logged-in user.
GET /api/tasks Login required Your tasks, newest first. Query: page, limit, done.
POST /api/tasks Login required Create a task. The title is required.
GET /api/tasks/:id Login required One of your tasks. A task of another user returns 404.
PUT /api/tasks/:id Login required Partial update: send only the fields to change.
DELETE /api/tasks/:id Login required Delete one of your tasks.

RECORDED REQUESTS

Real requests, real responses.

15 requests recorded from the API running on my laptop on October 20, 2026. Tokens are shortened.

01GET/health200Check that the server is up

REQUEST

GET /health

RESPONSE

HTTP 200 OK

{
  "status": "ok"
}
02POST/api/auth/register201Create an account

REQUEST

POST /api/auth/register
Content-Type: application/json

{
  "name": "Rafi",
  "email": "rafi.demo@example.com",
  "password": "rahasia123"
}

RESPONSE

HTTP 201 Created

{
  "data": {
    "id": 1,
    "name": "Rafi",
    "email": "rafi.demo@example.com",
    "created_at": "2026-10-20T15:38:56.913128552+07:00"
  },
  "message": "registrasi berhasil"
}
03POST/api/auth/register409Same email again is rejected

REQUEST

POST /api/auth/register
Content-Type: application/json

{
  "name": "Rafi",
  "email": "rafi.demo@example.com",
  "password": "rahasia123"
}

RESPONSE

HTTP 409 Conflict

{
  "error": "email sudah terdaftar"
}
04POST/api/auth/register400Invalid input is rejected

REQUEST

POST /api/auth/register
Content-Type: application/json

{
  "name": "Rafi",
  "email": "not-an-email",
  "password": "123"
}

RESPONSE

HTTP 400 Bad Request

{
  "error": "Key: 'registerInput.Email' Error:Field validation for 'Email' failed on the 'email' tag\nKey: 'registerInput.Password' Error:Field validation for 'Password' failed on the 'min' tag"
}
05POST/api/auth/login200Log in and receive a JWT

REQUEST

POST /api/auth/login
Content-Type: application/json

{
  "email": "rafi.demo@example.com",
  "password": "rahasia123"
}

RESPONSE

HTTP 200 OK

{
  "expires_in": 86400,
  "token": "eyJhbGciOiJIUzI1Ni…",
  "token_type": "Bearer"
}
06POST/api/auth/login401A wrong password gives 401

REQUEST

POST /api/auth/login
Content-Type: application/json

{
  "email": "rafi.demo@example.com",
  "password": "salah-total"
}

RESPONSE

HTTP 401 Unauthorized

{
  "error": "email atau password salah"
}
07GET/api/me200Profile of the logged-in user

REQUEST

GET /api/me
Authorization: Bearer eyJhbGciOiJIUzI1Ni…

RESPONSE

HTTP 200 OK

{
  "data": {
    "id": 1,
    "name": "Rafi",
    "email": "rafi.demo@example.com",
    "created_at": "2026-10-20T15:38:56.913128552+07:00"
  }
}
08GET/api/tasks401No token, no access

REQUEST

GET /api/tasks

RESPONSE

HTTP 401 Unauthorized

{
  "error": "token tidak ditemukan"
}
09POST/api/tasks201Create a task

REQUEST

POST /api/tasks
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1Ni…

{
  "title": "Belajar goroutine",
  "description": "Baca dokumentasi dan buat contoh"
}

RESPONSE

HTTP 201 Created

{
  "data": {
    "id": 1,
    "user_id": 1,
    "title": "Belajar goroutine",
    "description": "Baca dokumentasi dan buat contoh",
    "done": false,
    "created_at": "2026-10-20T15:38:57.067602292+07:00",
    "updated_at": "2026-10-20T15:38:57.067602292+07:00"
  }
}
10POST/api/tasks201Create a second task

REQUEST

POST /api/tasks
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1Ni…

{
  "title": "Menulis tes untuk handler"
}

RESPONSE

HTTP 201 Created

{
  "data": {
    "id": 2,
    "user_id": 1,
    "title": "Menulis tes untuk handler",
    "description": "",
    "done": false,
    "created_at": "2026-10-20T15:38:57.112172832+07:00",
    "updated_at": "2026-10-20T15:38:57.112172832+07:00"
  }
}
11GET/api/tasks?page=1&limit=10200List tasks with pagination

REQUEST

GET /api/tasks?page=1&limit=10
Authorization: Bearer eyJhbGciOiJIUzI1Ni…

RESPONSE

HTTP 200 OK

{
  "data": [
    {
      "id": 2,
      "user_id": 1,
      "title": "Menulis tes untuk handler",
      "description": "",
      "done": false,
      "created_at": "2026-10-20T15:38:57.112172832+07:00",
      "updated_at": "2026-10-20T15:38:57.112172832+07:00"
    },
    {
      "id": 1,
      "user_id": 1,
      "title": "Belajar goroutine",
      "description": "Baca dokumentasi dan buat contoh",
      "done": false,
      "created_at": "2026-10-20T15:38:57.067602292+07:00",
      "updated_at": "2026-10-20T15:38:57.067602292+07:00"
    }
  ],
  "meta": {
    "limit": 10,
    "page": 1,
    "total": 2
  }
}
12PUT/api/tasks/1200Update only one field

REQUEST

PUT /api/tasks/1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1Ni…

{
  "done": true
}

RESPONSE

HTTP 200 OK

{
  "data": {
    "id": 1,
    "user_id": 1,
    "title": "Belajar goroutine",
    "description": "Baca dokumentasi dan buat contoh",
    "done": true,
    "created_at": "2026-10-20T15:38:57.067602292+07:00",
    "updated_at": "2026-10-20T15:38:57.133681456+07:00"
  }
}
13GET/api/tasks?done=true200Filter by done

REQUEST

GET /api/tasks?done=true
Authorization: Bearer eyJhbGciOiJIUzI1Ni…

RESPONSE

HTTP 200 OK

{
  "data": [
    {
      "id": 1,
      "user_id": 1,
      "title": "Belajar goroutine",
      "description": "Baca dokumentasi dan buat contoh",
      "done": true,
      "created_at": "2026-10-20T15:38:57.067602292+07:00",
      "updated_at": "2026-10-20T15:38:57.133681456+07:00"
    }
  ],
  "meta": {
    "limit": 10,
    "page": 1,
    "total": 1
  }
}
14GET/api/tasks/9999404A task that does not exist

REQUEST

GET /api/tasks/9999
Authorization: Bearer eyJhbGciOiJIUzI1Ni…

RESPONSE

HTTP 404 Not Found

{
  "error": "task tidak ditemukan"
}
15DELETE/api/tasks/2200Delete a task

REQUEST

DELETE /api/tasks/2
Authorization: Bearer eyJhbGciOiJIUzI1Ni…

RESPONSE

HTTP 200 OK

{
  "message": "task dihapus"
}

DESIGN DECISIONS

Choices I made, and why.

01

Every task query is scoped to the logged-in user

A task is fetched with both its id and the user id from the token, so a user cannot read, change, or delete a task that belongs to someone else. Such a task returns 404, the same as a task that does not exist.

02

Partial updates with pointer fields

The update endpoint reads title, description, and done as pointers. That separates a field that was not sent from an empty value or false, so done: false is applied while an omitted field is left alone.

03

Passwords and tokens

Passwords are hashed with bcrypt and never appear in a response. Login gives the same message for a wrong email and a wrong password. Tokens are HS256 JWTs that expire after 24 hours, and the parser accepts only that signing method.

04

Pagination with a safe range

The list endpoint defaults to page 1 with 10 items, accepts a limit from 1 to 100 and falls back to 10 for anything else, and returns a meta object with page, limit, and total so a client can build its own pager.

05

Runs without a database server

GORM uses a pure-Go SQLite driver, so the API starts with go run and nothing else to install. The database is one file, configured with DB_PATH.

03 / NEXT IMPROVEMENTS

What is still missing.

The API has no automated tests yet, and the JWT secret falls back to a default value when the environment variable is not set, so a production setup should refuse to start without it. Next I would add handler tests, rate limiting on login, and a PostgreSQL option.

DISCUSS THIS PROJECT ↗ NEXT PROJECT: D'ROLLS ↗