带有 Go、Chi 和 InMemory Store 的 REST API 第一部分
什么是 REST API?
API(即应用程序编程接口)是一组规则,定义应用程序或设备如何相互连接和通信。REST API 是符合 REST(即表述性状态传输架构风格)设计原则的 API。因此,REST API 有时也称为 RESTful API。
本教程的重点是使用 Go 编写 REST API。
电影资源
我们将Movie
通过当前项目管理资源。它并不能准确地表示如何在实际系统中对电影资源进行建模,而只是一些基本类型以及如何在 REST API 中处理它们的混合。
场地 | 类型 |
---|---|
ID | 通用唯一标识符 |
标题 | 细绳 |
导演 | 细绳 |
导演 | 细绳 |
发布日期 | 时间 |
票价 | 浮动64 |
项目设置
为项目创建一个文件夹,我将其命名为,movies-api-with-go-chi-and-memory-store
但它通常是 GitHub 存储库的根目录,因此您可以适当地命名它,例如movies-api
.
go.mod
在终端执行以下命令进行初始化
go mod init github.com/kashifsoofi/blog-code-samples/movies-api-with-go-chi-and-memory-store
添加一个包含以下内容的新文件main.go
作为开始
package main
func main() {
println("Hello, World!")
}
项目结构
我喜欢添加子包将相关功能组合在一起。为此,我将在该store
文件夹中添加 3 个根级文件夹和 1 个子文件夹。文件夹结构如下(不显示文件)。
.
└── movies-api-with-go-chi-and-memory-store/
├── api - this will contain rest routes, handlers etc.
├── config - this will contain anything related to service configuration
└── store/ - this will contain store interface
我只有 2 个资源,health
但是movies
,如果您在单个休息服务中提供更多资源,请随时在 下为每个资源添加一个文件夹api
。store
如果您要添加多个stores
例如Postgres
和一个Redis
缓存以在点击之前进行检查,则同样如此Postgres
,然后随意为每个商店添加特定文件夹。
配置
添加名为 的文件夹config
和名为 的文件config.go
。我喜欢将所有应用程序配置保留在一个位置,并将使用优秀的envconfig
包来加载配置,同时为选项设置一些默认值。这个包允许我们从环境变量加载应用程序配置,使用标准 Go 包可以完成同样的事情,但在我看来,这个包提供了很好的抽象,同时又不失可读性。
package config
import (
"time"
"github.com/kelseyhightower/envconfig"
)
const envPrefix = ""
type Configuration struct {
HTTPServer
}
type HTTPServer struct {
IdleTimeout time.Duration `envconfig:"HTTP_SERVER_IDLE_TIMEOUT" default:"60s"`
Port int `envconfig:"PORT" default:"8080"`
ReadTimeout time.Duration `envconfig:"HTTP_SERVER_READ_TIMEOUT" default:"1s"`
WriteTimeout time.Duration `envconfig:"HTTP_SERVER_WRITE_TIMEOUT" default:"2s"`
}
func Load() (Configuration, error) {
var cfg Configuration
err := envconfig.Process(envPrefix, &cfg)
if err != nil {
return cfg, err
}
return cfg, nil
}
这将导致错误,可以通过在终端上执行以下命令来解决。
go mod tidy
Configuration
您可以通过转换为 aninterface
然后将配置添加到每个子包(例如等)来改进它api
。store
可以使用环境变量更新配置,例如在我们更新启动服务器后,在终端上执行以下操作将在端口 5000 上启动服务器main.go
。
PORT=5000 go run main.go
电影商店界面
添加一个名为 的新文件夹store
和一个名为 的文件movie_store.go
。我们将为我们的电影商店和支持结构添加一个接口。
package store
import (
"time"
"github.com/google/uuid"
)
type Movie struct {
ID uuid.UUID
Title string
Director string
ReleaseDate time.Time
TicketPrice float64
CreatedAt time.Time
UpdatedAt time.Time
}
type CreateMovieParams struct {
ID uuid.UUID
Title string
Director string
ReleaseDate time.Time
TicketPrice float64
}
type UpdateMovieParams struct {
Title string
Director string
ReleaseDate time.Time
TicketPrice float64
}
type Interface interface {
GetAll() ([]Movie, error)
GetByID(id uuid.UUID) (Movie, error)
Create(createMovieParams CreateMovieParams) error
Update(id uuid.UUID, updateMovieParams UpdateMovieParams) error
Delete(id uuid.UUID) error
}
还添加一个名为 的自定义应用程序错误文件errors.go
,这些文件使我们的商店包的客户端不知道所使用的存储技术,我们的存储实现会将任何本机错误转换为我们的业务错误,然后再返回给客户端。
package store
import (
"fmt"
"github.com/google/uuid"
)
type DuplicateKeyError struct {
ID uuid.UUID
}
func (e *DuplicateKeyError) Error() string {
return fmt.Sprintf("duplicate movie id: %v", e.ID)
}
type RecordNotFoundError struct{}
func (e *RecordNotFoundError) Error() string {
return "record not found"
}
记忆电影商店
memory_movies_store.go
添加一个名为in文件夹的新文件store
。添加一个MemoryMoviesStore
带有映射字段的结构体以将电影存储在内存中。还添加一个RWMutex
字段以避免对电影字段进行并发读/写访问。
我们将实现为store.Interface
添加/删除电影而定义的所有方法到结构的映射字段MemoryMoviesStore
。对于读取,我们锁定要读取的集合,读取结果并使用 释放锁定defer
。对于写入,我们获取写锁而不是读锁。
package store
import (
"sync"
"time"
"github.com/google/uuid"
)
type MemoryMoviesStore struct {
movies map[uuid.UUID]Movie
mu sync.RWMutex
}
func NewMemoryMoviesStore() *MemoryMoviesStore {
return &MemoryMoviesStore{
movies: map[uuid.UUID]Movie{},
}
}
func (s *MemoryMoviesStore) GetAll() ([]Movie, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var movies []Movie
for _, m := range s.movies {
movies = append(movies, m)
}
return movies, nil
}
func (s *MemoryMoviesStore) GetByID(id uuid.UUID) (Movie, error) {
s.mu.RLock()
defer s.mu.RUnlock()
m, ok := s.movies[id]
if !ok {
return Movie{}, &RecordNotFoundError{}
}
return m, nil
}
func (s *MemoryMoviesStore) Create(createMovieParams CreateMovieParams) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.movies[createMovieParams.ID]; ok {
return &DuplicateKeyError{ID: createMovieParams.ID}
}
movie := Movie{
ID: createMovieParams.ID,
Title: createMovieParams.Title,
Director: createMovieParams.Director,
ReleaseDate: createMovieParams.ReleaseDate,
TicketPrice: createMovieParams.TicketPrice,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
s.movies[movie.ID] = movie
return nil
}
func (s *MemoryMoviesStore) Update(id uuid.UUID, updateMovieParams UpdateMovieParams) error {
s.mu.Lock()
defer s.mu.Unlock()
m, ok := s.movies[id]
if !ok {
return &RecordNotFoundError{}
}
m.Title = updateMovieParams.Title
m.Director = updateMovieParams.Director
m.ReleaseDate = updateMovieParams.ReleaseDate
m.TicketPrice = updateMovieParams.TicketPrice
m.UpdatedAt = time.Now().UTC()
s.movies[id] = m
return nil
}
func (s *MemoryMoviesStore) Delete(id uuid.UUID) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.movies, id)
return nil
}
休息服务器
添加一个新文件夹以添加所有 REST api 服务器相关文件。我们首先添加server.go
文件并添加一个结构体来表示 REST 服务器。该结构将具有运行服务器、路由和所有依赖项所需的配置实例。还添加启动服务器的方法。
对于路由,我们将使用优秀的chi
路由器,它是一个轻量级、自动化且可组合的路由器,用于构建 HTTP 服务。
在 start 方法中,我们将构造一个标准包Server
提供的实例,前提是我们在方法中进行了设置。然后,我们将设置一个正常关闭的方法并调用以启动我们的 REST 服务器。net/http
chi mux
NewServer
ListenAndServe
package api
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/kashifsoofi/blog-code-samples/movies-api-with-go-chi-and-memory-store/config"
"github.com/kashifsoofi/blog-code-samples/movies-api-with-go-chi-and-memory-store/store"
"github.com/go-chi/chi/v5"
)
type Server struct {
cfg config.HTTPServer
store store.Interface
router *chi.Mux
}
func NewServer(cfg config.HTTPServer, store store.Interface) *Server {
srv := &Server{
cfg: cfg,
store: store,
router: chi.NewRouter(),
}
srv.routes()
return srv
}
func (s *Server) Start(ctx context.Context) {
server := http.Server{
Addr: fmt.Sprintf(":%d", s.cfg.Port),
Handler: s.router,
IdleTimeout: s.cfg.IdleTimeout,
ReadTimeout: s.cfg.ReadTimeout,
WriteTimeout: s.cfg.WriteTimeout,
}
shutdownComplete := handleShutdown(func() {
if err := server.Shutdown(ctx); err != nil {
log.Printf("server.Shutdown failed: %v\n", err)
}
})
if err := server.ListenAndServe(); err == http.ErrServerClosed {
<-shutdownComplete
} else {
log.Printf("http.ListenAndServe failed: %v\n", err)
}
log.Println("Shutdown gracefully")
}
func handleShutdown(onShutdownSignal func()) <-chan struct{} {
shutdown := make(chan struct{})
go func() {
shutdownSignal := make(chan os.Signal, 1)
signal.Notify(shutdownSignal, os.Interrupt, syscall.SIGTERM)
<-shutdownSignal
onShutdownSignal()
close(shutdown)
}()
return shutdown
}
自定义 API 错误
errors.go
我们将在文件夹下的文件中定义 REST 服务器返回的任何自定义错误api
。我已经在文件中添加了需要从该服务返回的所有错误。但实际上我们会从最常见的开始,然后在需要时添加新的。
package api
import (
"net/http"
"github.com/go-chi/render"
)
type ErrResponse struct {
Err error `json:"-"` // low-level runtime error
HTTPStatusCode int `json:"-"` // http response status code
StatusText string `json:"status"` // user-level status message
AppCode int64 `json:"code,omitempty"` // application-specific error code
ErrorText string `json:"error,omitempty"` // application-level error message, for debugging
}
func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
render.Status(r, e.HTTPStatusCode)
return nil
}
var (
ErrNotFound = &ErrResponse{HTTPStatusCode: 404, StatusText: "Resource not found."}
ErrBadRequest = &ErrResponse{HTTPStatusCode: 400, StatusText: "Bad request"}
ErrInternalServerError = &ErrResponse{HTTPStatusCode: 500, StatusText: "Internal Server Error"}
)
func ErrConflict(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 409,
StatusText: "Duplicate Key",
ErrorText: err.Error(),
}
}
路线
我喜欢将服务提供的所有路由保存在一个位置和一个名为routes.go
. 它更容易记住并减轻认知负担。
routes
方法挂在我们的Server
结构体上,定义了字段上的所有端点router
。我定义了一个/health
端点,它将返回该服务的当前运行状况。然后添加电影的子路由器组。这可以帮助我们将中间件仅应用于/api/movies
路由,例如身份验证、请求日志记录。
package api
import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func (s *Server) routes() {
s.router.Use(render.SetContentType(render.ContentTypeJSON))
s.router.Get("/health", s.handleGetHealth)
s.router.Route("/api/movies", func(r chi.Router) {
r.Get("/", s.handleListMovies)
r.Post("/", s.handleCreateMovie)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", s.handleGetMovie)
r.Put("/", s.handleUpdateMovie)
r.Delete("/", s.handleDeleteMovie)
})
})
}
请注意,所有处理程序都挂在Server
结构体之外,这有助于访问每个处理程序中所需的依赖项。structs
如果服务中有多个资源,则为每个资源单独添加仅包含该资源所需的依赖项可能是有意义的。
健康端点处理程序
我为资源添加了一个单独的文件health
。它有一个用于单个端点的处理程序,一个我们将作为响应发送的结构以及Renderer
响应结构的接口实现。
package api
import (
"net/http"
"github.com/go-chi/render"
)
type healthResponse struct {
OK bool `json:"ok"`
}
func (hr healthResponse) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (s *Server) handleGetHealth(w http.ResponseWriter, r *http.Request) {
health := healthResponse{OK: true}
render.Render(w, r, health)
}
电影端点处理程序
通过 ID 获取电影
让我们首先添加一个结构体,用于将 a 返回Movie
给 REST 服务的调用者,并实现Renderer
接口,以便我们可以使用Render
方法返回数据。
type movieResponse struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Director string `json:"director"`
ReleaseDate time.Time `json:"release_date"`
TicketPrice float64 `json:"ticket_price"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func NewMovieResponse(m store.Movie) movieResponse {
return movieResponse{
ID: m.ID,
Title: m.Title,
Director: m.Director,
ReleaseDate: m.ReleaseDate,
TicketPrice: m.TicketPrice,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
}
func (hr movieResponse) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
我喜欢更structs
接近使用它们的方法/包。它确实会导致一些代码重复,例如在这种情况下与文件中的 struct defindmovieResponse
非常相似,但这允许其余包不完全依赖于包,我们可以有不同的标签,例如结构中的 db 特定标签但不是结构中的。Movie
store/movies_store.go
store
store
movieResponse
现在是处理程序,我们接收 aResponseWriter
和 a Request
,我们id
使用URLParam
方法从路径中提取参数,如果解析失败我们渲染 a BadRequest
。
movie
然后我们继续获取如果在给定渲染store
的存储中找不到记录,如果返回的错误不是我们在存储包中定义的错误,那么我们渲染一个,我们可以添加更多自定义/已知错误来存储和翻译根据用例使用适当的 HTTP 状态代码。id
NotFound
InternalServerError
如果一切正常,那么我们将其转换store.Movie
为movieResponse
并渲染结果。结果将作为json
响应正文返回给调用者。
func (s *Server) handleGetMovie(w http.ResponseWriter, r *http.Request) {
idParam := chi.URLParam(r, "id")
id, err := uuid.Parse(idParam)
if err != nil {
render.Render(w, r, ErrBadRequest)
return
}
movie, err := s.store.GetByID(id)
if err != nil {
var rnfErr *store.RecordNotFoundError
if errors.As(err, &rnfErr) {
render.Render(w, r, ErrNotFound)
} else {
render.Render(w, r, ErrInternalServerError)
}
return
}
mr := NewMovieResponse(movie)
render.Render(w, r, mr)
}
获取所有/列表电影
movieResponse
对于响应,我们将使用我们定义的相同结构Get By ID
,我们只需添加一个新方法来创建数组/切片Renderer
func (s *Server) handleListMovies(w http.ResponseWriter, r *http.Request) {
movies, err := s.store.GetAll()
if err != nil {
render.Render(w, r, ErrInternalServerError)
return
}
render.RenderList(w, r, NewMovieListResponse(movies))
}
处理程序方法非常简单,我们调用GetAll
,如果错误返回InternalServerError
,否则返回电影列表。
func (s *Server) handleListMovies() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
movies, err := s.store.GetAll()
if err != nil {
render.Render(w, r, ErrInternalServerError)
return
}
render.RenderList(w, r, NewMovieListResponse(movies))
}
}
制作电影
与 get 相同,我们首先添加一个新的结构来接收创建新电影所需的参数。但是,Renderer
我们不是实现Binder
接口,而是实现接口,如果需要,可以在Bind
方法中完成自定义映射,例如添加元数据或CreatedBy
从JWT
令牌设置字段。
请注意,在这个结构中我们没有CreatedAt
and 。UpdatedAt
type CreateMovieRequest struct {
ID string `json:"id"`
Title string `json:"title"`
Director string `json:"director"`
ReleaseDate time.Time `json:"release_date"`
TicketPrice float64 `json:"ticket_price"`
}
func (mr *createMovieRequest) Bind(r *http.Request) error {
return nil
}
在处理程序中,我们将请求正文绑定到我们的结构,如果Bind
成功,则将其转换为方法CreateMovieParams
所需的结构,并调用方法将电影添加到数据存储中。如果存在重复键错误,我们将返回未知错误,如果全部成功,我们将返回。store.Create
Create
409 Conflict
500 InternalServerError
200 OK
func (s *Server) handleCreateMovie(w http.ResponseWriter, r *http.Request) {
data := &CreateMovieRequest{}
if err := render.Bind(r, data); err != nil {
render.Render(w, r, ErrBadRequest)
return
}
createMovieParams := store.CreateMovieParams{
ID: uuid.MustParse(data.ID),
Title: data.Title,
Director: data.Director,
ReleaseDate: data.ReleaseDate,
TicketPrice: data.TicketPrice,
}
err := s.store.Create(createMovieParams)
if err != nil {
var dupKeyErr *store.DuplicateKeyError
if errors.As(err, &dupKeyErr) {
render.Render(w, r, ErrConflict(err))
} else {
render.Render(w, r, ErrInternalServerError)
}
return
}
w.WriteHeader(200)
w.Write(nil)
}
更新电影
与上面相同Create Movie
,我们引入了一个新的结构体updateMovieRequest
来接收更新电影所需的参数,并Binder
为该结构体实现了接口。
type updateMovieRequest struct {
Title string `json:"title"`
Director string `json:"director"`
ReleaseDate time.Time `json:"release_date"`
TicketPrice float64 `json:"ticket_price"`
}
func (mr *updateMovieRequest) Bind(r *http.Request) error {
return nil
}
在hander中,我们读取id
from路径,然后绑定请求体中的结构。如果没有错误,那么我们将请求转换为store.UpdateMovieParams
并调用Update
store 方法来更新电影。200 OK
如果更新成功,我们将返回。
func (s *Server) handleUpdateMovie(w http.ResponseWriter, r *http.Request) {
idParam := chi.URLParam(r, "id")
id, err := uuid.Parse(idParam)
if err != nil {
render.Render(w, r, ErrBadRequest)
return
}
data := &updateMovieRequest{}
if err := render.Bind(r, data); err != nil {
render.Render(w, r, ErrBadRequest)
return
}
updateMovieParams := store.UpdateMovieParams{
Title: data.Title,
Director: data.Director,
ReleaseDate: data.ReleaseDate,
TicketPrice: data.TicketPrice,
}
err = s.store.Update(id, updateMovieParams)
if err != nil {
var rnfErr *store.RecordNotFoundError
if errors.As(err, &rnfErr) {
render.Render(w, r, ErrNotFound)
} else {
render.Render(w, r, ErrInternalServerError)
}
return
}
w.WriteHeader(200)
w.Write(nil)
}
删除影片
这可能是最简单的处理程序,因为它不需要任何Renderer
or Binder
,我们只需id
从路径中获取,然后调用Delete
store 方法来删除资源。如果删除成功我们返回200 OK
。
func (s *Server) handleDeleteMovie(w http.ResponseWriter, r *http.Request) {
idParam := chi.URLParam(r, "id")
id, err := uuid.Parse(idParam)
if err != nil {
render.Render(w, r, ErrBadRequest)
return
}
err = s.store.Delete(id)
if err != nil {
var rnfErr *store.RecordNotFoundError
if errors.As(err, &rnfErr) {
render.Render(w, r, ErrNotFound)
} else {
render.Render(w, r, ErrInternalServerError)
}
return
}
w.WriteHeader(200)
w.Write(nil)
}
启动服务器
现在一切都已设置完毕,是时候更新main
方法了。首先加载配置,然后创建 的实例MemoryMoviesStore
,在这里我们还可以实例化我们的服务器所依赖的任何其他依赖项。下一步是创建 struct 的实例api.Server
并调用该Start
方法来启动服务器。服务器将开始侦听配置的端口,您可以使用curl
或调用端点Postman
。
package main
import (
"context"
"log"
"github.com/kashifsoofi/blog-code-samples/movies-api-with-go-chi-and-memory-store/api"
"github.com/kashifsoofi/blog-code-samples/movies-api-with-go-chi-and-memory-store/config"
"github.com/kashifsoofi/blog-code-samples/movies-api-with-go-chi-and-memory-store/store"
)
func main() {
ctx := context.Background()
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
store := store.NewMemoryMoviesStore()
server := api.NewServer(cfg.HTTPServer, store)
server.Start(ctx)
}
测试
我将列出手动测试 api 端点的步骤,因为我们没有Swagger UI
或任何其他与之交互的 UI,Postman
也可用于测试端点。
- 启动服务器执行以下命令
go run main.go
按顺序执行以下测试,如果您在 8080 以外的端口上运行,请记住更新端口。
注意: 我没有在下面的回复中添加
created_at
和updated_at
字段。
测试
获取全部返回空列表
要求
curl --request GET --url "http://localhost:8080/api/movies"
预期反应
[]
按 ID 获取应该返回 Not Found
要求
curl --request GET --url "http://localhost:8080/api/movies/1"
预期反应
[]
按 ID 获取应该返回 Not Found
要求
curl --request GET --url "http://localhost:8080/api/movies/1"
预期反应
[]
按 ID 获取应该返回 Not Found
要求
curl --request GET --url "http://localhost:8080/api/movies/1"
预期反应
[]
通过 ID 获取无效 ID
要求
curl --request GET --url "http://localhost:8080/api/movies/1"
预期反应
{"status":"Bad request"}
通过ID获取不存在的记录
要求
curl --request GET --url "http://localhost:8080/api/movies/98268a96-a6ac-444f-852a-c6472129aa22"
预期反应
{"status":"Resource not found."}
制作电影
要求
curl --request POST --data '{ "id": "98268a96-a6ac-444f-852a-c6472129aa22", "title": "Star Wars: Episode I – The Phantom Menace", "director": "George Lucas", "release_date": "1999-05-16T01:01:01.00Z", "ticket_price": 10.70 }' --url "http://localhost:8080/api/movies"
预期反应
使用现有 ID 创建电影
要求
curl --request POST --data '{ "id": "98268a96-a6ac-444f-852a-c6472129aa22", "title": "Star Wars: Episode I – The Phantom Menace", "director": "George Lucas", "release_date": "1999-05-16T01:01:01.00Z", "ticket_price": 10.70 }' --url "http://localhost:8080/api/movies"
预期反应
{"status":"Duplicate Key","error":"duplicate movie id: 98268a96-a6ac-444f-852a-c6472129aa22"}
获取所有电影
要求
curl --request GET --url "http://localhost:8080/api/movies"
预期反应
[{"id":"98268a96-a6ac-444f-852a-c6472129aa22","title":"Star Wars: Episode I – The Phantom Menace","director":"George Lucas","release_date":"1999-05-16T01:01:01Z","ticket_price":10.7}]
通过 ID 获取电影
要求
curl --request GET --url "http://localhost:8080/api/movies/98268a96-a6ac-444f-852a-c6472129aa22"
预期反应
{"id":"98268a96-a6ac-444f-852a-c6472129aa22","title":"Star Wars: Episode I – The Phantom Menace","director":"George Lucas","release_date":"1999-05-16T01:01:01Z","ticket_price":10.7}
更新电影
要求
curl --request PUT --data '{ "title": "Star Wars: Episode I – The Phantom Menace", "director": "George Lucas", "release_date": "1999-05-16T01:01:01.00Z", "ticket_price": 20.70 }' --url "http://localhost:8080/api/movies/98268a96-a6ac-444f-852a-c6472129aa22"
预期反应
通过 ID 获取电影 - 获取更新记录
要求
curl --request GET --url "http://localhost:8080/api/movies/98268a96-a6ac-444f-852a-c6472129aa22"
预期反应
{"id":"98268a96-a6ac-444f-852a-c6472129aa22","title":"Star Wars: Episode I – The Phantom Menace","director":"George Lucas","release_date":"1999-05-16T01:01:01Z","ticket_price":20.7}
删除影片
要求
curl --request DELETE --url "http://localhost:8080/api/movies/98268a96-a6ac-444f-852a-c6472129aa22"
预期反应
按 ID 获取电影 - 已删除的记录
要求
curl --request GET --url "http://localhost:8080/api/movies/98268a96-a6ac-444f-852a-c6472129aa22"
预期反应
{"status":"Resource not found."}
来源
演示应用程序的源代码托管在 GitHub 上的blog-code-samples存储库中。
- 点赞
- 收藏
- 关注作者
评论(0)