fix: change API design to be more RESTful

This commit is contained in:
Muyao CHEN 2024-10-07 23:25:25 +02:00
parent c1173b4bcc
commit cb7a4bf5c5
4 changed files with 142 additions and 18 deletions

View File

@ -13,6 +13,9 @@
- [Version](#version)
- [2024/10/03](#20241003)
- [2024/10/04](#20241004)
- [2024/10/06](#20241006)
- [Workflow](#workflow)
- [2024/10/07](#20241007)
<!--toc:end-->
A tricount like expense-sharing system written in Go
@ -241,3 +244,69 @@ rest, I just put unit tests aside for later.
That should be the correct workflow. But to save time, I will cut off the
integration test part (the 2nd point).
### 2024/10/07
I rethought about the whole API design (even though I have only one yet). I
have created `/signup` and `/login` without thinking too much, but in fact
it is not quite *RESTful*.
**REST** is all about resources. While `/signup` and `/login` is quite
comprehensible, thus service-oriented, they don't follow the REST philosophy,
that is to say, **resource-oriented**.
If we rethink about `/signup`, what it does is to create a resource of `User`.
Thus, for a backend API, it'd better be named as `User.Create`. But what
about `/login`, it doesn't do anything about `User`. It would be strange to
declare it as a User-relevant method.
Instead, what `/login` really does, is to **create a session**.
In consequence, we have to create a new struct `Session` that can be created,
deleted, or updated.
It might seem overkill, and in real life, even in the official Pet store
example of OpenAPI, signup and login are under /user. But it just opened my
mind and forces me to **think and design RESTfully**!
That being said, for the user side, we shall still have `/signup` and `/login`,
because on the Front-end, we must be user-centered. We can even make this
2 functions on the same page with the same endpoint `/login`. The user enter
the email and the password, then clicks on `Login or Signup`. If the login
is successful, then he is logged in. Otherwise, if the user doesn't exist
yet, we open up 2 more inputs (first name and last name) for signup. They
can just provide the extra information and click again on `Signup`.
That, again, being said, I am thinking about doing some Front-end stuff just
to make the validation tests of the product simpler.
#### The choice of the front end framework
I have considered several choices.
If I didn't purposely make the backend code to provide a REST API, I might
choose server-side-rendering with `templ + htmx`, or even `template+vanilla
javascript`.
I can still write a rather static Go-frontend-server to serve HTMLs and call
my Go backend. *And it might be a good idea if they communicate on Go native
rpc.* It worth a try.
And I have moved on to `Svelte` which seems very simple by design and the
whole compile thing makes it really charm. But this is mainly a Go project,
to learn something new with a rather small community means potentially more
investment. I can learn it later.
Among `Angular`, `React` and `Vue`, I prefer `Vue`, for several reasons.
First, `Angular` is clearly overkill for this small demo project. Second,
`React` is good but I personally like the way of Vue doing things. And I
work with Vue at work, so I might have more technical help from my colleagues.
So the plan for this week is to have both the Front end part and Backend part
working, just for user signup and login.
I would like to directly put this stuff on a CI-pipeline for tests and
deployment, even I have barely nothing yet. It is always good to do this
preparation stuff at the early stage of the project. So we can benefit from
them all the way along.
Moreover, even I am not really finishing the project, it can still be
something representable that I can show to a future interviewer.

View File

@ -37,28 +37,79 @@ servers:
- url: https:/localhost:8000/v1
tags:
- name: user
- name: session
paths:
/user/signup:
/user/create:
post:
tags:
- user
description: Sign up as a new user
description: Create a new user
requestBody:
description: Sign up
description: Create a new user
content:
application/json:
schema:
$ref: '#/components/schemas/UserSignUpRequest'
$ref: '#/components/schemas/UserCreateRequest'
responses:
'200':
description: Successful operation
'400':
description: Client side error
content:
application/json:
schema:
type: object
properties:
code:
type: string
example: FailedOperation.UserExisted
message:
type: string
example: "Email already existed."
'500':
description: Server side error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrResponse'
/session/create:
post:
tags:
- user
description: Create a new session for a user
requestBody:
description: Create session
content:
application/json:
schema:
$ref: '#/components/schemas/SessionCreateRequest'
responses:
'200':
description: Successful operation
headers:
X-Expires-After:
description: date in UTC when token expires
schema:
type: string
format: date-time
content:
application/json:
schema:
type: string
'400':
description: Client side error
content:
application/json:
schema:
type: object
properties:
code:
type: string
example: AuthFailure
message:
type: string
example: "wrong email password."
'500':
description: Server side error
content:
@ -68,7 +119,7 @@ paths:
components:
schemas:
UserSignUpRequest:
UserCreateRequest:
type: object
properties:
email:
@ -88,12 +139,24 @@ components:
- fist_name
- last_name
- password
SessionCreateRequest:
type: object
properties:
email:
type: string
example: bruce@wayne.com
password:
type: string
example: verystrongpassword
required:
- email
- password
ErrResponse:
type: object
properties:
code:
type: string
example: FailedOperation.UserExisted
example: InternalError
message:
type: string
example: "Email already existed."
example: "Server internal error."

View File

@ -36,10 +36,8 @@ import (
// User is the user controller interface, it describes all the handlers
// that need to be implemented for the /user endpoint
type User interface {
Signup(core.Context)
Create(core.Context)
UpdateInfo(*gin.Context)
Login(*gin.Context)
Logout(*gin.Context)
ChangePassword(*gin.Context)
}
@ -59,7 +57,7 @@ func NewUserController(us usecase.User) User {
}
}
func (uc *UserController) Signup(ctx core.Context) {
func (uc *UserController) Create(ctx core.Context) {
var params model.User
if err := ctx.Bind(&params); err != nil {
@ -87,11 +85,5 @@ func (uc *UserController) Signup(ctx core.Context) {
func (uc *UserController) UpdateInfo(ctx *gin.Context) {
}
func (uc *UserController) Login(ctx *gin.Context) {
}
func (uc *UserController) Logout(ctx *gin.Context) {
}
func (uc *UserController) ChangePassword(ctx *gin.Context) {
}

View File

@ -52,7 +52,7 @@ func Routes(engine *gin.Engine, c controller.AppController) *gin.Engine {
{
userV1 := v1.Group("/user")
{
userV1.POST("/signup", func(ctx *gin.Context) { c.User.Signup(ctx) })
userV1.POST("/create", func(ctx *gin.Context) { c.User.Create(ctx) })
}
}