fix: change API design to be more RESTful
This commit is contained in:
parent
c1173b4bcc
commit
cb7a4bf5c5
69
README.md
69
README.md
@ -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.
|
||||
|
@ -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."
|
||||
|
@ -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(¶ms); 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) {
|
||||
}
|
||||
|
@ -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) })
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user