Cara Menggunakan Spectral dengan TypeScript

Audrey Lopez

Audrey Lopez

19 May 2025

Cara Menggunakan Spectral dengan TypeScript

Menjaga konsistensi, kualitas, dan kepatuhan terhadap standar desain sangat penting bagi tim yang menggunakan API. Spesifikasi API seperti OpenAPI dan AsyncAPI menyediakan cetak biru (blueprint), tetapi memastikan cetak biru ini diikuti dengan benar di berbagai layanan dan tim bisa menjadi tantangan yang menakutkan. Di sinilah alat linting API berperan, dan Spectral menonjol sebagai opsi sumber terbuka yang fleksibel dan kuat. Ketika dikombinasikan dengan TypeScript, Spectral memberdayakan pengembang untuk membuat aturan kustom yang tangguh dan aman tipe (type-safe), meningkatkan tata kelola API ke tingkat yang baru.

Tutorial ini akan memandu Anda melalui proses memanfaatkan Spectral dengan TypeScript, mulai dari pengaturan awal hingga membuat logika validasi kustom yang canggih. Kita akan menjelajahi bagaimana TypeScript meningkatkan pengembangan aturan Spectral, menghasilkan solusi linting API yang lebih mudah dipelihara dan lebih andal.

💡
Ingin alat Pengujian API hebat yang menghasilkan Dokumentasi API yang indah?

Ingin platform Terintegrasi, All-in-One untuk Tim Pengembang Anda bekerja sama dengan produktivitas maksimum?

Apidog memenuhi semua permintaan Anda, dan menggantikan Postman dengan harga yang jauh lebih terjangkau!
button

Memahami Spectral: Penjaga Spesifikasi API Anda

Sebelum masuk ke integrasi TypeScript, mari kita pahami apa itu Spectral dan mengapa ini adalah alat yang berharga dalam toolkit pengembangan API Anda.

Spectral adalah linter JSON/YAML sumber terbuka dengan fokus utama pada format deskripsi API seperti OpenAPI (v2 dan v3) dan AsyncAPI. Tujuannya adalah untuk membantu menegakkan pedoman desain API, mendeteksi kesalahan umum, dan memastikan konsistensi di seluruh lanskap API Anda. Anggap saja sebagai ESLint atau TSLint, tetapi khusus untuk kontrak API Anda.

Manfaat Utama Menggunakan Spectral:

Spectral beroperasi berdasarkan ruleset. Ruleset adalah kumpulan aturan, di mana setiap aturan menargetkan bagian tertentu dari dokumen API Anda (menggunakan ekspresi JSONPath) dan menerapkan logika validasi. Spectral dilengkapi dengan ruleset bawaan (misalnya, spectral:oas untuk standar OpenAPI), tetapi kekuatan sebenarnya terletak pada kemampuan untuk menentukan ruleset kustom.

Mengapa TypeScript untuk Aturan Kustom Spectral?

Meskipun ruleset Spectral dapat didefinisikan dalam YAML atau JavaScript (file .js), menggunakan TypeScript untuk mengembangkan fungsi kustom menawarkan keuntungan signifikan:

Dengan menulis fungsi Spectral kustom Anda dalam TypeScript, Anda membawa ketelitian dan manfaat perkakas yang sama ke kode tata kelola API Anda seperti yang Anda lakukan pada kode aplikasi Anda.

Menyiapkan Lingkungan Spectral dan TypeScript Anda

Mari kita mulai dan siapkan alat yang diperlukan.

Prasyarat:

Langkah-langkah Instalasi:

Pertama, Anda memerlukan CLI Spectral untuk menjalankan operasi linting dan menguji aturan Anda. Seringkali berguna untuk menginstalnya secara global atau menggunakan npx.Bash

npm install -g @stoplight/spectral-cli
# atau
yarn global add @stoplight/spectral-cli

Untuk mengembangkan aturan kustom secara terprogram dan menggunakan pustaka inti Spectral dalam proyek TypeScript, instal paket Spectral yang diperlukan:Bashnpm install @stoplight/spectral-core @stoplight/spectral-functions @stoplight/spectral-rulesets typescript ts-node --save-dev # atau yarn add @stoplight/spectral-core @stoplight/spectral-functions @stoplight/spectral-rulesets typescript ts-node --dev

Mari kita uraikan paket-paket ini:

Konfigurasi TypeScript:

Buat file tsconfig.json di root proyek Anda jika Anda belum memilikinya. Konfigurasi dasar mungkin terlihat seperti ini:JSON

{
  "compilerOptions": {
    "target": "es2020", // Atau versi yang lebih baru
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist", // Direktori output untuk JavaScript yang dikompilasi
    "rootDir": "./src", // Direktori sumber untuk file TypeScript Anda
    "resolveJsonModule": true // Memungkinkan mengimpor file JSON
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

Sesuaikan outDir dan rootDir sesuai dengan struktur proyek Anda. Kita akan mengasumsikan fungsi TypeScript kustom Anda akan berada di direktori src.

Konsep Inti Spectral: Aturan, Ruleset, dan Fungsi

Sebelum menulis fungsi TypeScript, mari kita perkuat pemahaman kita tentang komponen utama Spectral.

Aturan:

Sebuah aturan mendefinisikan pemeriksaan spesifik yang akan dilakukan. Properti utama dari sebuah aturan meliputi:

Ruleset:

Ruleset adalah file YAML atau JavaScript (misalnya, .spectral.yaml, .spectral.js, atau .spectral.ts ketika dikompilasi) yang mengelompokkan aturan. Ia juga dapat:

Fungsi:

Fungsi adalah unit logika inti yang melakukan validasi sebenarnya. Spectral menyediakan banyak fungsi bawaan seperti:

Kekuatan sebenarnya muncul ketika ini tidak cukup, dan Anda perlu menulis fungsi kustom Anda sendiri – di sinilah TypeScript bersinar.

Membuat Fungsi Spectral Kustom Pertama Anda di TypeScript

Mari kita buat fungsi kustom sederhana. Bayangkan kita ingin menegakkan aturan bahwa semua ringkasan operasi API harus dalam format Title Case dan tidak melebihi 70 karakter.

Langkah 1: Definisikan Fungsi Kustom di TypeScript

Buat file, misalnya src/customFunctions.ts:TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';

interface TitleCaseLengthOptions {
  maxLength: number;
}

// Fungsi kustom untuk memeriksa apakah string dalam format title-cased dan dalam panjang maksimum
export const titleCaseAndLength: IFunction<string, TitleCaseLengthOptions> = (
  targetVal,
  options,
  context
): IFunctionResult[] | void => {
  const results: IFunctionResult[] = [];

  if (typeof targetVal !== 'string') {
    // Seharusnya tidak terjadi jika path 'given' menunjuk ke string, tetapi praktik yang baik
    return [{ message: `Nilai pada path '${context.path.join('.')}' harus berupa string.` }];
  }

  // Periksa Title Case (pemeriksaan sederhana: huruf pertama setiap kata adalah huruf besar)
  const words = targetVal.split(' ');
  const isTitleCase = words.every(word => word.length === 0 || (word[0] === word[0].toUpperCase() && (word.length === 1 || word.substring(1) === word.substring(1).toLowerCase())));

  if (!isTitleCase) {
    results.push({
      message: `Ringkasan "${targetVal}" pada path '${context.path.join('.')}' harus dalam Title Case.`,
      path: [...context.path], // Path ke elemen yang melanggar
    });
  }

  // Periksa panjang
  const maxLength = options?.maxLength || 70; // Default ke 70 jika tidak disediakan
  if (targetVal.length > maxLength) {
    results.push({
      message: `Ringkasan "${targetVal}" pada path '${context.path.join('.')}' melebihi panjang maksimum ${maxLength} karakter. Panjang saat ini: ${targetVal.length}.`,
      path: [...context.path],
    });
  }

  return results;
};

Penjelasan:

Langkah 2: Kompilasi Fungsi TypeScript

Jika Anda berencana menggunakan fungsi ini dengan ruleset .spectral.yaml atau .spectral.js yang menunjuk ke functionsDir, Anda perlu mengkompilasi TypeScript Anda ke JavaScript.

Tambahkan skrip build ke package.json Anda:JSON

{
  "scripts": {
    "build": "tsc"
  }
}

Jalankan npm run build atau yarn build. Ini akan mengkompilasi src/customFunctions.ts ke dist/customFunctions.js (berdasarkan tsconfig.json kita).

Langkah 3: Buat File Ruleset

Mari kita buat ruleset, misalnya, .spectral.js (atau .spectral.ts jika Anda lebih suka pengaturan yang sepenuhnya berbasis TypeScript, lihat bagian selanjutnya).

Jika menggunakan file ruleset JavaScript yang langsung mereferensikan fungsi yang dikompilasi:JavaScript

// .spectral.js
const { titleCaseAndLength } = require('./dist/customFunctions'); // Path ke fungsi JS yang dikompilasi

module.exports = {
  extends: [['@stoplight/spectral-rulesets/dist/rulesets/oas', 'recommended']],
  rules: {
    'operation-summary-title-case-length': {
      description: 'Ringkasan operasi harus dalam format title-cased dan tidak melebihi 70 karakter.',
      message: '{{error}}', // Pesan akan berasal dari fungsi kustom
      given: '$.paths[*][*].summary', // Menargetkan semua ringkasan operasi
      severity: 'warn',
      formats: ["oas3"], // Terapkan hanya untuk dokumen OpenAPI v3
      then: {
        function: titleCaseAndLength, // Langsung mereferensikan fungsi yang diimpor
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    // Anda dapat menambahkan aturan lain di sini
  },
};

Atau, jika menggunakan .spectral.yaml dan functionsDir:

Pertama, pastikan direktori dist Anda berisi index.js yang mengekspor fungsi Anda, atau customFunctions.js Anda langsung mengekspornya. Misalnya, jika dist/customFunctions.js memiliki exports.titleCaseAndLength = ...;, Anda bisa melakukan:YAML

# .spectral.yaml
extends:
  - ["@stoplight/spectral-rulesets/dist/rulesets/oas", "recommended"]
functionsDir: "./dist" # Direktori yang berisi fungsi JS kustom yang dikompilasi
rules:
  operation-summary-title-case-length:
    description: "Ringkasan operasi harus dalam format title-cased dan tidak melebihi 70 karakter."
    message: "{{error}}"
    given: "$.paths[*][*].summary"
    severity: "warn"
    formats: ["oas3"]
    then:
      function: customFunctions#titleCaseAndLength # Mengasumsikan fungsi diekspor dari customFunctions.js
      functionOptions:
        maxLength: 70

Di sini, customFunctions#titleCaseAndLength memberitahu Spectral untuk mencari customFunctions.js (atau customFunctions/index.js) di functionsDir dan menggunakan fungsi titleCaseAndLength yang diekspor.

Langkah 4: Buat Dokumen OpenAPI Sampel

Mari kita buat file openapi.yaml sederhana untuk menguji aturan kita:YAML

# openapi.yaml
openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
paths:
  /items:
    get:
      summary: retrieves all items from the store # Salah: bukan title case
      responses:
        '200':
          description: Daftar item.
    post:
      summary: Adds A New Item To The Ever Expanding Collection Of Items In The Store # Salah: terlalu panjang
      responses:
        '201':
          description: Item dibuat.
  /users:
    get:
      summary: Get User Details # Benar
      responses:
        '200':
          description: Detail pengguna.

Langkah 5: Jalankan Spectral

Sekarang, jalankan Spectral CLI terhadap dokumen OpenAPI Anda:Bash

spectral lint openapi.yaml --ruleset .spectral.js
# atau jika menggunakan ruleset YAML (seringkali terdeteksi otomatis jika bernama .spectral.yaml)
spectral lint openapi.yaml

Output yang Diharapkan:

Anda akan melihat peringatan yang mirip dengan ini:

openapi.yaml
 2:10  warning  operation-summary-title-case-length  Summary "retrieves all items from the store" at path 'paths./items.get.summary' must be in Title Case.   paths./items.get.summary
 6:10  warning  operation-summary-title-case-length  Summary "Adds A New Item To The Ever Expanding Collection Of Items In The Store" at path 'paths./items.post.summary' exceeds maximum length of 70 characters. Current length: 78.  paths./items.post.summary

✖ 2 problems (0 errors, 2 warnings, 0 infos, 0 hints)

Output ini mengkonfirmasi bahwa fungsi TypeScript kustom kita, yang dikompilasi ke JavaScript, dengan benar mengidentifikasi pelanggaran.

Penggunaan Spectral Secara Terprogram dengan Ruleset TypeScript

Untuk skenario yang lebih kompleks atau integrasi yang lebih erat ke dalam aplikasi, Anda mungkin ingin menggunakan Spectral secara terprogram dan mendefinisikan ruleset Anda sepenuhnya dalam TypeScript. Ini melewati kebutuhan akan file .spectral.yaml atau .spectral.js terpisah jika diinginkan, dan memungkinkan konstruksi ruleset yang dinamis.

Buat file TypeScript untuk linter Anda, misalnya src/linter.ts:TypeScript

import { Spectral, Document } from '@stoplight/spectral-core';
import { oas } from '@stoplight/spectral-rulesets';
import { truthy } from '@stoplight/spectral-functions'; // Contoh fungsi bawaan
import { titleCaseAndLength } from './customFunctions'; // Fungsi TS kustom Anda
import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
import * as fs from 'fs/promises';
import * as path from 'path';

// Definisikan instance Spectral
const spectral = new Spectral();

// Muat ruleset dan fungsi bawaan
spectral.setRuleset({
  extends: [[oas, 'recommended']], // Memperluas aturan OpenAPI yang direkomendasikan
  rules: {
    'operation-summary-title-case-length': {
      description: 'Ringkasan operasi harus dalam format title-cased dan tidak melebihi 70 karakter.',
      message: '{{error}}',
      given: '$.paths[*][*].summary',
      severity: 'warn',
      formats: ["oas3"],
      then: {
        function: titleCaseAndLength, // Langsung menggunakan fungsi TypeScript
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    'info-contact-defined': { // Contoh menggunakan fungsi bawaan
        description: 'Objek info harus memiliki objek kontak.',
        message: 'Informasi kontak API hilang.',
        given: '$.info',
        severity: 'warn',
        formats: ["oas3"],
        then: {
          field: 'contact',
          function: truthy, // Menggunakan fungsi bawaan
        },
      },
  },
});

// Fungsi untuk melakukan linting pada dokumen
export async function lintDocument(filePath: string): Promise<ISpectralDiagnostic[]> {
  try {
    const absolutePath = path.resolve(filePath);
    const fileContent = await fs.readFile(absolutePath, 'utf-8');
    
    // Buat objek Dokumen Spectral
    // Argumen kedua (URI) penting untuk menyelesaikan $refs relatif jika ada
    const document = new Document(fileContent, undefined, absolutePath); 
    
    const results = await spectral.run(document);
    return results;
  } catch (error) {
    console.error('Error linting dokumen:', error);
    return [];
  }
}

// Contoh penggunaan (misalnya, dalam skrip atau modul lain)
async function main() {
  const diagnostics = await lintDocument('openapi.yaml'); // Path ke spesifikasi API Anda
  if (diagnostics.length > 0) {
    console.log('Ditemukan Masalah Linting API:');
    diagnostics.forEach(issue => {
      console.log(
        `- [${issue.severity === 0 ? 'Error' : issue.severity === 1 ? 'Warning' : issue.severity === 2 ? 'Info' : 'Hint'}] ${issue.code} (${issue.message}) pada ${issue.path.join('.')}`
      );
    });
  } else {
    console.log('Tidak ada masalah linting API ditemukan. Kerja bagus!');
  }
}

// Jika Anda ingin menjalankan file ini langsung dengan ts-node
if (require.main === module) {
  main().catch(console.error);
}

Untuk menjalankan ini:Bash

npx ts-node src/linter.ts

Pendekatan ini memberikan fleksibilitas maksimum:

Teknik Fungsi Kustom Tingkat Lanjut

Mari kita jelajahi beberapa aspek yang lebih canggih dalam membuat fungsi kustom.

Fungsi Kustom Asinkron:

Jika fungsi kustom Anda perlu melakukan operasi asinkron (misalnya, mengambil sumber daya eksternal untuk divalidasi, meskipun gunakan dengan hati-hati untuk kinerja), Anda dapat mendefinisikannya sebagai fungsi async.TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';

export const checkExternalResource: IFunction<string, { url: string }> = 
  async (targetVal, options, context): Promise<IFunctionResult[] | void> => {
  try {
    const response = await fetch(`${options.url}/${targetVal}`);
    if (!response.ok) {
      return [{ message: `Sumber daya '${targetVal}' tidak ditemukan di ${options.url}. Status: ${response.status}` }];
    }
  } catch (error: any) {
    return [{ message: `Error mengambil sumber daya '${targetVal}': ${error.message}` }];
  }
};

Spectral akan dengan benar await fungsi asinkron Anda.

Mengakses Dokumen Lengkap dan Nilai yang Diselesaikan:

Objek context.document memberi Anda akses ke seluruh dokumen yang di-parse. Lebih kuat lagi, context.document.resolved menyediakan versi dokumen yang sepenuhnya di-dereference, yang penting saat berurusan dengan pointer $ref.TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
import {isPlainObject} from '@stoplight/json';

// Contoh: Pastikan skema yang direferensikan memiliki properti spesifik
export const referencedSchemaHasProperty: IFunction<{$ref: string}, { propertyName: string }> = (
  targetVal, // Ini akan menjadi objek seperti { $ref: '#/components/schemas/MySchema' }
  options,
  context
): IFunctionResult[] | void => {
  if (!targetVal.$ref) return;

  // Metode `context.document.resolveAnchor` dapat menemukan nilai yang diselesaikan untuk $ref
  const resolvedValue = context.document.resolveAnchor(targetVal.$ref);

  if (!resolvedValue || !isPlainObject(resolvedValue.value)) {
    return [{ message: `Tidak dapat menyelesaikan $ref: ${targetVal.$ref}` }];
  }
  
  const schemaProperties = resolvedValue.value.properties as Record<string, unknown> | undefined;

  if (!schemaProperties || schemaProperties[options.propertyName] === undefined) {
    return [{
      message: `Skema yang direferensikan oleh "${targetVal.$ref}" pada path '${context.path.join('.')}' harus memiliki properti "${options.propertyName}".`,
      path: [...context.path, '$ref'] // Arahkan ke $ref itu sendiri
    }];
  }
};

Fungsi ini akan digunakan dengan path given yang menargetkan objek yang berisi $ref, misalnya: $.paths[*].*.responses.*.content.*.schema.

Bekerja dengan format:

Pastikan aturan kustom Anda menentukan format yang berlaku (misalnya, oas2, oas3, asyncapi2). Ini mencegah aturan berjalan pada tipe dokumen yang tidak kompatibel. Anda dapat mengakses format yang terdeteksi dalam fungsi Anda melalui context.document.formats.

Mengorganisir dan Memperluas Ruleset

Seiring bertambahnya koleksi aturan kustom Anda, organisasi menjadi kunci.

// .spectral.js (ruleset utama)
module.exports = {
  extends: ['./rulesets/common-rules.js', './rulesets/security-rules.js'],
  // ... aturan atau override lainnya
};

Mengintegrasikan Spectral ke dalam Alur Kerja Anda

Kekuatan sebenarnya dari linting API terwujud ketika diotomatiskan.

# Contoh langkah GitHub Action
- name: Lint API Specification
  run: spectral lint ./path/to/your/api-spec.yaml --ruleset ./.spectral.js --fail-severity=error

Praktik Terbaik untuk Spectral dan Aturan Kustom TypeScript

Kesimpulan: Tingkatkan Tata Kelola API Anda dengan Spectral dan TypeScript

Spectral menawarkan kerangka kerja yang tangguh untuk linting API, dan dengan memanfaatkan TypeScript untuk pengembangan aturan kustom, Anda membawa peningkatan keamanan tipe, pengalaman pengembang, dan kemudahan pemeliharaan ke strategi tata kelola API Anda. Baik Anda menegakkan praktik terbaik OpenAPI, konvensi penamaan khusus perusahaan, atau logika bisnis kompleks dalam desain API Anda, kombinasi mesin aturan Spectral yang fleksibel dan sistem pengetikan yang kuat dari TypeScript menyediakan solusi yang ampuh.

Dengan mengintegrasikan Spectral ke dalam siklus pengembangan Anda, mulai dari umpan balik IDE hingga pipeline CI/CD, Anda dapat memastikan API Anda konsisten, berkualitas tinggi, dan mematuhi standar yang Anda definisikan, yang pada akhirnya menghasilkan API yang lebih andal dan lebih mudah dikonsumsi. Mulailah dari yang kecil, ulangi ruleset Anda, dan berdayakan tim Anda dengan alat untuk membangun API yang lebih baik.

Mengembangkan API dengan Apidog

Apidog adalah alat pengembangan API yang membantu Anda mengembangkan API dengan lebih mudah dan efisien.