带有 Go、Chi 和 InMemory Store 的 REST API 第一部分

举报
Q神 发表于 2023/06/24 15:14:42 2023/06/24
【摘要】 什么是 REST API?API(即应用程序编程接口)是一组规则,定义应用程序或设备如何相互连接和通信。REST API 是符合 REST(即表述性状态传输架构风格)设计原则的 API。因此,REST API 有时也称为 RESTful API。本教程的重点是使用 Go 编写 REST API。电影资源我们将Movie通过当前项目管理资源。它并不能准确地表示如何在实际系​​统中对电影资源进行...

什么是 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,如果您在单个休息服务中提供更多资源,请随时在 下为每个资源添加一个文件夹apistore如果您要添加多个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然后将配置添加到每个子包(例如等)来改进它apistore

可以使用环境变量更新配置,例如在我们更新启动服务器后,在终端上执行以下操作将在端口 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/httpchi muxNewServerListenAndServe

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 特定标签但不是结构中的。Moviestore/movies_store.gostorestoremovieResponse

现在是处理程序,我们接收 aResponseWriter和 a Request,我们id使用URLParam方法从路径中提取参数,如果解析失败我们渲染 a BadRequest

movie然后我们继续获取如果在给定渲染store的存储中找不到记录,如果返回的错误不是我们在存储包中定义的错误,那么我们渲染一个,我们可以添加更多自定义/已知错误来存储和翻译根据用例使用适当的 HTTP 状态代码。idNotFoundInternalServerError

如果一切正常,那么我们将其转换store.MoviemovieResponse并渲染结果。结果将作为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方法中完成自定义映射,例如添加元数据或CreatedByJWT令牌设置字段。

请注意,在这个结构中我们没有CreatedAtand 。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.CreateCreate409 Conflict500 InternalServerError200 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中,我们读取idfrom路径,然后绑定请求体中的结构。如果没有错误,那么我们将请求转换为store.UpdateMovieParams并调用Updatestore 方法来更新电影。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)
}

删除影片

这可能是最简单的处理程序,因为它不需要任何Rendereror Binder,我们只需id从路径中获取,然后调用Deletestore 方法来删​​除资源。如果删除成功我们返回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_atupdated_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存储库中。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。