Pada satu ketika, setiap syarikat mencapai persimpangan di mana mereka perlu berhenti dan menilai semula alatan yang mereka gunakan. Bagi kami, detik itu tiba apabila kami menyedari bahawa API yang menjanakan papan pemuka web kami telah menjadi tidak terurus, sukar untuk diuji dan tidak memenuhi piawaian yang kami tetapkan untuk pangkalan kod kami.
Arcjet terutamanya keselamatan sebagai SDK kod untuk membantu pembangun melaksanakan fungsi keselamatan seperti pengesanan bot, pengesahan e-mel dan pengesanan PII. Ini berkomunikasi dengan API gRPC keputusan berprestasi tinggi dan kependaman rendah kami.
Papan pemuka web kami menggunakan API REST yang berasingan terutamanya untuk mengurus sambungan tapak dan menyemak analitis permintaan yang diproses, namun ini juga termasuk mendaftar pengguna baharu dan mengurus akaun mereka, yang bermaksud ia masih merupakan bahagian penting dalam produk.
Tangkapan skrin halaman analisis permintaan papan pemuka Arcjet.
Jadi, kami memutuskan untuk menyahut cabaran membina semula API kami dari bawah—kali ini dengan fokus pada kebolehselenggaraan, prestasi dan kebolehskalaan. Walau bagaimanapun, kami tidak mahu memulakan projek penulisan semula yang besar - yang tidak pernah berjaya - sebaliknya, kami memutuskan untuk membina asas baharu dan kemudian bermula dengan satu titik akhir API.
Dalam siaran ini, saya akan membincangkan cara kami menghampiri perkara ini.
Apabila kelajuan menjadi keutamaan kami, Next.js menyediakan penyelesaian paling mudah untuk membina titik akhir API yang boleh digunakan bahagian hadapan kami. Ia memberi kami pembangunan tindanan penuh yang lancar dalam satu pangkalan kod, dan kami tidak perlu terlalu risau tentang infrastruktur kerana kami menggunakan Vercel.
Tumpuan kami adalah pada keselamatan kami sebagai SDK kod dan API keputusan kependaman rendah jadi untuk papan pemuka bahagian hadapan tindanan ini membolehkan kami membuat prototaip ciri dengan cepat dengan sedikit geseran.
Timbunan Kami: Next.js, DrizzleORM, useSWR, NextAuth
Walau bagaimanapun, apabila produk kami berkembang, kami mendapati bahawa menggabungkan semua titik akhir API kami dan kod hujung hadapan dalam projek yang sama membawa kepada keadaan kusut masai.
Menguji API kami menjadi rumit (dan sangat sukar dilakukan dengan Next.js pula), dan kami memerlukan sistem yang boleh mengendalikan kedua-dua dalaman dan luaran penggunaan. Semasa kami menyepadukan dengan lebih banyak platform (seperti Vercel, Fly.io dan Netlify), kami menyedari bahawa kelajuan pembangunan sahaja tidak mencukupi. Kami memerlukan penyelesaian yang lebih mantap.
Sebagai sebahagian daripada projek ini, kami juga ingin menangani kebimbangan keselamatan yang berlarutan tentang cara Vercel memerlukan anda mendedahkan pangkalan data anda secara terbuka. Melainkan anda membayar untuk "pengiraan selamat" perusahaan mereka, menyambung ke pangkalan data jauh memerlukannya mempunyai titik akhir awam. Kami lebih suka mengunci pangkalan data kami supaya ia hanya boleh diakses melalui rangkaian peribadi. Pertahanan secara mendalam adalah penting dan ini akan menjadi satu lagi lapisan perlindungan.
Ini menyebabkan kami memutuskan untuk menyahganding UI bahagian hadapan daripada API bahagian belakang.
Apakah itu "API Emas"? Ia bukan teknologi atau rangka kerja khusus—ia lebih kepada set prinsip ideal yang mentakrifkan API yang dibina dengan baik. Walaupun pembangun mungkin mempunyai pilihan mereka sendiri untuk bahasa dan rangka kerja, terdapat konsep tertentu yang sah merentas susunan teknologi yang kebanyakannya boleh bersetuju untuk membina API berkualiti tinggi.
Kami sudah mempunyai pengalaman dalam menyampaikan API berprestasi tinggi . API Keputusan kami digunakan berdekatan dengan pelanggan kami, menggunakan Kubernetes untuk membuat skala secara dinamik dan dioptimumkan untuk respons kependaman rendah .
Kami menganggap persekitaran tanpa pelayan dan penyedia lain, tetapi dengan kluster k8s kami yang sedia ada telah beroperasi, adalah wajar untuk menggunakan semula infrastruktur yang sedia ada: penempatan melalui Octopus Deploy, pemantauan melalui Grafana Jaeger, Loki, Prometheus, dll.
Selepas Rust vs Go dalaman yang singkat dibakar, kami memilih Go untuk kesederhanaan, kelajuan dan sejauh mana ia mencapai matlamat asalnya sokongan yang sangat baik untuk membina perkhidmatan rangkaian berskala . Kami juga sudah menggunakannya untuk API Keputusan dan memahami cara mengendalikan API Go, yang memuktamadkan keputusan untuk kami.
Menukar API bahagian belakang kepada Go adalah mudah kerana kesederhanaan dan ketersediaan alatan yang hebat. Tetapi terdapat satu tangkapan: kami mengekalkan bahagian hadapan Next.js dan tidak mahu menulis jenis TypeScript secara manual atau mengekalkan dokumentasi berasingan untuk API baharu kami.
Masukkan OpenAPI—sempurna untuk keperluan kita. OpenAPI membolehkan kami mentakrifkan kontrak antara bahagian hadapan dan bahagian belakang, sambil turut berfungsi sebagai dokumentasi kami. Ini menyelesaikan masalah mengekalkan skema untuk kedua-dua belah apl.
Menyepadukan pengesahan dalam Go tidaklah terlalu sukar, terima kasih kepada NextAuth yang agak mudah untuk ditiru pada bahagian belakang. NextAuth (kini Auth.js) mempunyai API yang tersedia untuk mengesahkan sesi.
Ini bermakna kami boleh mempunyai klien TypeScript di bahagian hadapan yang dijana daripada spesifikasi OpenAPI kami yang membuat panggilan pengambilan ke API bahagian belakang. Bukti kelayakan disertakan secara automatik dalam panggilan ambil dan bahagian belakang boleh mengesahkan sesi dengan NextAuth.
Menulis sebarang jenis ujian dalam Go adalah sangat mudah dan terdapat banyak contoh di luar sana, merangkumi topik menguji pengendali HTTP.
Adalah lebih mudah untuk menulis ujian untuk titik akhir Go API baharu berbanding API Next.js, terutamanya kerana kami ingin menguji panggilan keadaan disahkan dan pangkalan data sebenar. Kami dapat dengan mudah menulis ujian untuk penghala Gin dan memutarkan ujian penyepaduan sebenar terhadap pangkalan data Postgres kami menggunakan Testcontainers.
Kami bermula dengan menulis spesifikasi OpenAPI 3.0 untuk API kami. Pendekatan OpenAPI-first menggalakkan reka bentuk kontrak API sebelum pelaksanaan, memastikan semua pihak berkepentingan (pembangun, pengurus produk dan pelanggan) bersetuju dengan gelagat dan struktur API sebelum sebarang kod ditulis. Ia menggalakkan perancangan yang teliti dan menghasilkan reka bentuk API yang difikirkan dengan baik yang konsisten dan mematuhi amalan terbaik yang telah ditetapkan. Inilah sebab mengapa kami memilih untuk menulis spesifikasi dahulu dan menjana kod daripadanya, berbanding sebaliknya.
Alat pilihan saya untuk ini ialahAPI Fiddle, yang membantu anda mendraf dan menguji spesifikasi OpenAPI dengan cepat. Walau bagaimanapun, API Fiddle hanya menyokong OpenAPI 3.1 (yang kami tidak boleh gunakan kerana banyak perpustakaan belum menerima pakainya), jadi kami kekal dengan versi 3.0 dan menulis spesifikasi dengan tangan.
Berikut ialah contoh rupa spesifikasi untuk API kami:
openapi: 3.0.0 info: title: Arcjet Sites API description: A CRUD API to manage sites. version: 1.0.0 servers: - url: <https://api.arcjet.com/v1> description: Base URL for all API operations paths: /teams/{teamId}/sites: get: operationId: GetTeamSites summary: Get a list of sites for a team description: Returns a list of all Sites associated with a given Team. parameters: - name: teamId in: path required: true description: The ID of the team schema: type: string responses: "200": description: A list of sites content: application/json: schema: type: array items: $ref: "#/components/schemas/Site" default: description: unexpected error content: application/json: schema: $ref: "#/components/schemas/Error" components: schemas: Site: type: object properties: id: type: string description: The ID of the site name: type: string description: The name of the site teamId: type: string description: The ID of the team this site belongs to createdAt: type: string format: date-time description: The timestamp when the site was created updatedAt: type: string format: date-time description: The timestamp when the site was last updated deletedAt: type: string format: date-time nullable: true description: The timestamp when the site was deleted (if applicable) Error: required: - code - message - details properties: code: type: integer format: int32 description: Error code message: type: string description: Error message details: type: string description: Details that can help resolve the issue
Dengan menggunakan spesifikasi OpenAPI, kami menggunakan OAPI-codegen, alat yang menjana kod Go secara automatik daripada spesifikasi OpenAPI. Ia menjana semua jenis, pengendali dan struktur pengendalian ralat yang diperlukan, menjadikan proses pembangunan lebih lancar.
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../../api.yaml
Output ialah satu set fail Go, satu mengandungi rangka pelayan dan satu lagi dengan pelaksanaan pengendali. Berikut ialah contoh jenis Go yang dijana untuk objek Site:
// Site defines model for Site. type Site struct { // CreatedAt The timestamp when the site was created CreatedAt *time.Time `json:"createdAt,omitempty"` // DeletedAt The timestamp when the site was deleted (if applicable) DeletedAt *time.Time `json:"deletedAt"` // Id The ID of the site Id *string `json:"id,omitempty"` // Name The name of the site Name *string `json:"name,omitempty"` // TeamId The ID of the team this site belongs to TeamId *string `json:"teamId,omitempty"` // UpdatedAt The timestamp when the site was last updated UpdatedAt *time.Time `json:"updatedAt,omitempty"` }
Dengan adanya kod yang dijana, kami dapat melaksanakan logik pengendali API, seperti ini:
func (s Server) GetTeamSites(w http.ResponseWriter, r *http.Request, teamId string) { ctx := r.Context() // Check user has permission to access team resources isAllowed, err := s.userIsAllowed(ctx, teamId) if err != nil { slog.ErrorContext( ctx, "failed to check permissions", slogext.Err("err", err), slog.String("teamId", teamId), ) SendInternalServerError(ctx, w) return } if !isAllowed { SendForbidden(ctx, w) return } // Retrieve sites from database sites, err := s.repo.GetSitesForTeam(ctx, teamId) if err != nil { slog.ErrorContext( ctx, "list sites for team query returned an error", slogext.Err("err", err), slog.String("teamId", teamId), ) SendInternalServerError(ctx, w) return } SendOk(ctx, w, sites) }
Drizzle ialah ORM yang hebat untuk projek JS dan kami akan menggunakannya semula, tetapi mengalihkan kod pangkalan data daripada Next.js bermakna kami memerlukan sesuatu yang serupa untuk Go.
Kami memilih GORM sebagai ORM kami dan menggunakan Corak Repositori untuk abstrak interaksi pangkalan data. Ini membolehkan kami menulis pertanyaan pangkalan data yang bersih dan boleh diuji.
type ApiRepo interface { GetSitesForTeam(ctx context.Context, teamId string) ([]Site, error) } type apiRepo struct { db *gorm.DB } func (r apiRepo) GetSitesForTeam(ctx context.Context, teamId string) ([]Site, error) { var sites []Site result := r.db.WithContext(ctx).Where("team_id = ?", teamId).Find(&sites) if result.Error != nil { return nil, ErrorNotFound } return sites, nil }
Ujian adalah kritikal bagi kami. Kami ingin memastikan bahawa semua panggilan pangkalan data telah diuji dengan betul, jadi kami menggunakan Testcontainers untuk memutar pangkalan data sebenar untuk ujian kami, mencerminkan dengan teliti persediaan pengeluaran kami.
openapi: 3.0.0 info: title: Arcjet Sites API description: A CRUD API to manage sites. version: 1.0.0 servers: - url: <https://api.arcjet.com/v1> description: Base URL for all API operations paths: /teams/{teamId}/sites: get: operationId: GetTeamSites summary: Get a list of sites for a team description: Returns a list of all Sites associated with a given Team. parameters: - name: teamId in: path required: true description: The ID of the team schema: type: string responses: "200": description: A list of sites content: application/json: schema: type: array items: $ref: "#/components/schemas/Site" default: description: unexpected error content: application/json: schema: $ref: "#/components/schemas/Error" components: schemas: Site: type: object properties: id: type: string description: The ID of the site name: type: string description: The name of the site teamId: type: string description: The ID of the team this site belongs to createdAt: type: string format: date-time description: The timestamp when the site was created updatedAt: type: string format: date-time description: The timestamp when the site was last updated deletedAt: type: string format: date-time nullable: true description: The timestamp when the site was deleted (if applicable) Error: required: - code - message - details properties: code: type: integer format: int32 description: Error code message: type: string description: Error message details: type: string description: Details that can help resolve the issue
Selepas menyediakan persekitaran ujian, kami menguji semua operasi CRUD seperti yang kami lakukan dalam pengeluaran, memastikan kod kami berfungsi dengan betul.
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../../api.yaml
Untuk menguji pengendali API kami, kami menggunakan pakej httptest Go dan mengejek interaksi pangkalan data menggunakan Mockery. Ini membolehkan kami menumpukan pada menguji logik API tanpa perlu risau tentang isu pangkalan data.
// Site defines model for Site. type Site struct { // CreatedAt The timestamp when the site was created CreatedAt *time.Time `json:"createdAt,omitempty"` // DeletedAt The timestamp when the site was deleted (if applicable) DeletedAt *time.Time `json:"deletedAt"` // Id The ID of the site Id *string `json:"id,omitempty"` // Name The name of the site Name *string `json:"name,omitempty"` // TeamId The ID of the team this site belongs to TeamId *string `json:"teamId,omitempty"` // UpdatedAt The timestamp when the site was last updated UpdatedAt *time.Time `json:"updatedAt,omitempty"` }
Setelah API kami diuji dan digunakan, kami menumpukan perhatian kepada bahagian hadapan.
Panggilan API kami sebelum ini dibuat menggunakan AP ambilan yang disyorkan Next.jsI dengan caching terbina dalam. Untuk paparan yang lebih dinamik, beberapa komponen menggunakan SWR di atas pengambilan jadi kami boleh mendapatkan data yang selamat jenis, muat semula automatik, mengambil panggilan.
Untuk menggunakan API pada bahagian hadapan, kami menggunakan pustaka openapi-typescript, yang menjana jenis TypeScript berdasarkan skema OpenAPI kami. Ini memudahkan untuk menyepadukan bahagian belakang dengan bahagian hadapan kami tanpa perlu menyegerakkan model data secara manual. Ini mempunyai Tanstack Query terbina dalam, yang menggunakan pengambilan di bawah hud, tetapi turut disegerakkan ke skema kami.
Kami memindahkan titik akhir API secara beransur-ansur ke pelayan Go baharu dan membuat peningkatan kecil di sepanjang jalan. Jika anda membuka pemeriksa penyemak imbas anda, anda akan melihat permintaan baharu tersebut pergi ke api.arcjet.com
Tangkapan skrin pemeriksa penyemak imbas menunjukkan panggilan API ke hujung belakang Go baharu.
Jadi, adakah kita mencapai Golden API yang sukar difahami? Mari tandai kotak:
Kami pergi lebih jauh dengan:
Akhirnya, kami gembira dengan hasilnya. API kami lebih pantas, lebih selamat dan lebih baik diuji. Peralihan kepada Go adalah berbaloi dan kami kini berada pada kedudukan yang lebih baik untuk meningkatkan dan mengekalkan API kami apabila produk kami berkembang.
Atas ialah kandungan terperinci Memikirkan semula REST API kami: Membina API Emas. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!